53 changed files with 3607 additions and 106 deletions
@ -0,0 +1,76 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 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 #entityAliasForm="ngForm" [formGroup]="entityAliasFormGroup" (ngSubmit)="save()" style="width: 600px;"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2>{{ (isAdd ? 'alias.add' : 'alias.edit') | translate }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-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"> |
||||
|
<div fxLayout="row"> |
||||
|
<mat-form-field fxFlex class="mat-block"> |
||||
|
<mat-label translate>alias.name</mat-label> |
||||
|
<input matInput formControlName="alias" required> |
||||
|
<mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('required')"> |
||||
|
{{ 'alias.name-required' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('duplicateAliasName')"> |
||||
|
{{ 'alias.duplicate-alias' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<section class="tb-resolve-multiple-switch" fxLayout="column" fxLayoutAlign="start center"> |
||||
|
<label class="tb-small resolve-multiple-label" translate>alias.resolve-multiple</label> |
||||
|
<mat-slide-toggle class="resolve-multiple-switch" |
||||
|
formControlName="resolveMultiple"> |
||||
|
</mat-slide-toggle> |
||||
|
</section> |
||||
|
</div> |
||||
|
<tb-entity-filter formControlName="filter" |
||||
|
[resolveMultiple]="entityAliasFormGroup.get('resolveMultiple').value" |
||||
|
(resolveMultipleChanged)="entityAliasFormGroup.get('resolveMultiple').patchValue($event)" |
||||
|
[allowedEntityTypes]="allowedEntityTypes"> |
||||
|
</tb-entity-filter> |
||||
|
<tb-error [error]="entityAliasFormGroup.hasError('noEntityMatched') |
||||
|
? translate.instant('alias.entity-filter-no-entity-matched') : ''"></tb-error> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row"> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || entityAliasFormGroup.invalid || !entityAliasFormGroup.dirty"> |
||||
|
{{ (isAdd ? 'action.add' : 'action.save') | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
style="margin-right: 20px;" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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-resolve-multiple-switch { |
||||
|
padding-left: 10px; |
||||
|
|
||||
|
.resolve-multiple-switch { |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.resolve-multiple-label { |
||||
|
margin: 5px 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,164 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
FormArray, |
||||
|
FormBuilder, |
||||
|
FormControl, |
||||
|
FormGroup, |
||||
|
FormGroupDirective, |
||||
|
NgForm, Validators, ValidatorFn |
||||
|
} from '@angular/forms'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DialogComponent } from '@app/shared/components/dialog.component'; |
||||
|
import { AttributeData } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models'; |
||||
|
import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models'; |
||||
|
import { AliasEntityType, EntityType } from '@shared/models/entity-type.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 { EntityService } from '@core/http/entity.service'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
|
||||
|
export interface EntityAliasDialogData { |
||||
|
isAdd: boolean; |
||||
|
allowedEntityTypes: Array<EntityType | AliasEntityType>; |
||||
|
entityAliases: EntityAliases | Array<EntityAlias>; |
||||
|
alias?: EntityAlias; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entity-alias-dialog', |
||||
|
templateUrl: './entity-alias-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasDialogComponent}], |
||||
|
styleUrls: ['./entity-alias-dialog.component.scss'] |
||||
|
}) |
||||
|
export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialogComponent, EntityAlias> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
isAdd: boolean; |
||||
|
allowedEntityTypes: Array<EntityType | AliasEntityType>; |
||||
|
entityAliases: Array<EntityAlias>; |
||||
|
|
||||
|
alias: EntityAlias; |
||||
|
|
||||
|
entityAliasFormGroup: FormGroup; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: EntityAliasDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<EntityAliasDialogComponent, EntityAlias>, |
||||
|
private fb: FormBuilder, |
||||
|
private utils: UtilsService, |
||||
|
private translate: TranslateService, |
||||
|
private entityService: EntityService) { |
||||
|
super(store, router, dialogRef); |
||||
|
this.isAdd = data.isAdd; |
||||
|
this.allowedEntityTypes = data.allowedEntityTypes; |
||||
|
if (Array.isArray(data.entityAliases)) { |
||||
|
this.entityAliases = data.entityAliases; |
||||
|
} else { |
||||
|
this.entityAliases = []; |
||||
|
for (const aliasId of Object.keys(data.entityAliases)) { |
||||
|
this.entityAliases.push(data.entityAliases[aliasId]); |
||||
|
} |
||||
|
} |
||||
|
if (this.isAdd) { |
||||
|
this.alias = { |
||||
|
id: null, |
||||
|
alias: '', |
||||
|
filter: { |
||||
|
resolveMultiple: false |
||||
|
} |
||||
|
}; |
||||
|
} else { |
||||
|
this.alias = data.alias; |
||||
|
} |
||||
|
|
||||
|
this.entityAliasFormGroup = this.fb.group({ |
||||
|
alias: [this.alias.alias, [this.validateDuplicateAliasName(), Validators.required]], |
||||
|
resolveMultiple: [this.alias.filter.resolveMultiple], |
||||
|
filter: [this.alias.filter, Validators.required] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
validateDuplicateAliasName(): ValidatorFn { |
||||
|
return (c: FormControl) => { |
||||
|
const newAlias = c.value; |
||||
|
const found = this.entityAliases.find((entityAlias) => entityAlias.alias === newAlias); |
||||
|
if (found) { |
||||
|
if (this.isAdd || this.alias.id !== found.id) { |
||||
|
return { |
||||
|
duplicateAliasName: { |
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
private validate(): Observable<any> { |
||||
|
return this.entityService.resolveAliasFilter(this.alias.filter, null, 1, true); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.submitted = true; |
||||
|
this.alias.alias = this.entityAliasFormGroup.get('alias').value; |
||||
|
this.alias.filter = this.entityAliasFormGroup.get('filter').value; |
||||
|
this.alias.filter.resolveMultiple = this.entityAliasFormGroup.get('resolveMultiple').value; |
||||
|
this.validate().subscribe(() => { |
||||
|
if (this.isAdd) { |
||||
|
this.alias.id = this.utils.guid(); |
||||
|
} |
||||
|
this.dialogRef.close(this.alias); |
||||
|
}, |
||||
|
() => { |
||||
|
this.entityAliasFormGroup.setErrors({ |
||||
|
noEntityMatched: true |
||||
|
}); |
||||
|
const changesSubscriptuion = this.entityAliasFormGroup.valueChanges.subscribe(() => { |
||||
|
this.entityAliasFormGroup.setErrors(null); |
||||
|
changesSubscriptuion.unsubscribe(); |
||||
|
}); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,111 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 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 #entityAliasesForm="ngForm" [formGroup]="entityAliasesFormGroup" (ngSubmit)="save()" style="width: 700px;"> |
||||
|
<mat-toolbar fxLayout="row" color="primary"> |
||||
|
<h2>{{ title | translate }}</h2> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-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 class="tb-aliases-header" fxFlex fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<span fxFlex="5"></span> |
||||
|
<div fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<span class="tb-header-label" translate fxFlex="150px">alias.name</span> |
||||
|
<span class="tb-header-label" translate fxFlex style="padding-left: 10px;">alias.entity-filter</span> |
||||
|
<span class="tb-header-label" translate fxFlex="120px" style="padding-left: 10px;">alias.resolve-multiple</span> |
||||
|
<span style="min-width: 80px;"></span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<mat-divider></mat-divider> |
||||
|
<div mat-dialog-content> |
||||
|
<fieldset [disabled]="isLoading$ | async"> |
||||
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center" |
||||
|
formArrayName="entityAliases" |
||||
|
*ngFor="let entityAliasControl of entityAliasesFormGroup.get('entityAliases').controls; let $index = index"> |
||||
|
<span fxFlex="5">{{$index + 1}}.</span> |
||||
|
<div class="mat-elevation-z4 tb-alias" fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block" fxFlex="150px"> |
||||
|
<mat-label></mat-label> |
||||
|
<input matInput [formControl]="entityAliasControl.get('alias')" required placeholder="{{ 'entity.alias' | translate }}"> |
||||
|
<mat-error *ngIf="entityAliasControl.get('alias').hasError('required')"> |
||||
|
{{ 'entity.alias-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<tb-entity-filter-view fxFlex style="padding-left: 10px;" [formControl]="entityAliasControl.get('filter')"> |
||||
|
</tb-entity-filter-view> |
||||
|
<section fxFlex="120px" style="padding-left: 10px;" |
||||
|
class="tb-resolve-multiple-switch" |
||||
|
fxLayout="column" |
||||
|
fxLayoutAlign="center center"> |
||||
|
<mat-slide-toggle class="resolve-multiple-switch" |
||||
|
[formControl]="entityAliasControl.get('resolveMultiple')"> |
||||
|
</mat-slide-toggle> |
||||
|
</section> |
||||
|
<button [disabled]="isLoading$ | async" |
||||
|
mat-button mat-icon-button color="primary" |
||||
|
style="min-width: 40px;" |
||||
|
type="button" |
||||
|
(click)="editAlias($index)" |
||||
|
matTooltip="{{ 'alias.edit' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>edit</mat-icon> |
||||
|
</button> |
||||
|
<button [disabled]="isLoading$ | async" |
||||
|
mat-button mat-icon-button color="primary" |
||||
|
style="min-width: 40px;" |
||||
|
type="button" |
||||
|
(click)="removeAlias($index)" |
||||
|
matTooltip="{{ 'entity.remove-alias' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayout="row"> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="button" |
||||
|
(click)="addAlias()" |
||||
|
[fxShow]="!disableAdd" |
||||
|
[disabled]="isLoading$ | async" |
||||
|
matTooltip="{{ 'alias.add' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'alias.add' | translate }} |
||||
|
</button> |
||||
|
<span fxFlex></span> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || entityAliasesFormGroup.invalid || !entityAliasesFormGroup.dirty"> |
||||
|
{{ 'action.save' | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
style="margin-right: 20px;" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,49 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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-aliases-header { |
||||
|
min-height: 40px; |
||||
|
padding: 0 34px 0 34px; |
||||
|
margin: 5px; |
||||
|
|
||||
|
.tb-header-label { |
||||
|
font-size: 14px; |
||||
|
color: rgba(0, 0, 0, .570588); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-alias { |
||||
|
padding: 0 0 0 10px; |
||||
|
margin: 5px; |
||||
|
|
||||
|
.tb-resolve-multiple-switch { |
||||
|
padding-left: 10px; |
||||
|
|
||||
|
.resolve-multiple-switch { |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-dialog-content { |
||||
|
padding-top: 0 !important; |
||||
|
padding-bottom: 0 !important; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,264 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
||||
|
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 { AttributeData } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { EntityAlias, EntityAliases, EntityAliasFilter } from '@shared/models/alias.models'; |
||||
|
import { DatasourceType, Widget, widgetType } from '@shared/models/widget.models'; |
||||
|
import { AliasEntityType, EntityType } from '@shared/models/entity-type.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 { MatDialog } from '@angular/material/dialog'; |
||||
|
import { EntityAliasDialogComponent, EntityAliasDialogData } from './entity-alias-dialog.component'; |
||||
|
|
||||
|
export interface EntityAliasesDialogData { |
||||
|
entityAliases: EntityAliases; |
||||
|
widgets: Array<Widget>; |
||||
|
isSingleEntityAlias: boolean; |
||||
|
isSingleWidget?: boolean; |
||||
|
allowedEntityTypes?: Array<AliasEntityType>; |
||||
|
disableAdd?: boolean; |
||||
|
singleEntityAlias?: EntityAlias; |
||||
|
customTitle?: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entity-aliases-dialog', |
||||
|
templateUrl: './entity-aliases-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: EntityAliasesDialogComponent}], |
||||
|
styleUrls: ['./entity-aliases-dialog.component.scss'] |
||||
|
}) |
||||
|
export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesDialogComponent, EntityAliases> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
title: string; |
||||
|
disableAdd: boolean; |
||||
|
allowedEntityTypes: Array<EntityType | AliasEntityType>; |
||||
|
|
||||
|
aliasToWidgetsMap: {[aliasId: string]: Array<string>} = {}; |
||||
|
|
||||
|
entityAliasesFormGroup: FormGroup; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: EntityAliasesDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<EntityAliasesDialogComponent, EntityAliases>, |
||||
|
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 : 'entity.aliases'; |
||||
|
this.disableAdd = this.data.disableAdd; |
||||
|
this.allowedEntityTypes = this.data.allowedEntityTypes; |
||||
|
|
||||
|
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 aliasId of Object.keys(this.data.entityAliases)) { |
||||
|
this.aliasToWidgetsMap[aliasId] = widgetsTitleList; |
||||
|
} |
||||
|
} else { |
||||
|
this.data.widgets.forEach((widget) => { |
||||
|
if (widget.type === widgetType.rpc) { |
||||
|
if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) { |
||||
|
const targetDeviceAliasId = widget.config.targetDeviceAliasIds[0]; |
||||
|
widgetsTitleList = this.aliasToWidgetsMap[targetDeviceAliasId]; |
||||
|
if (!widgetsTitleList) { |
||||
|
widgetsTitleList = []; |
||||
|
this.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList; |
||||
|
} |
||||
|
widgetsTitleList.push(widget.config.title); |
||||
|
} |
||||
|
} else { |
||||
|
const datasources = this.utils.validateDatasources(widget.config.datasources); |
||||
|
datasources.forEach((datasource) => { |
||||
|
if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { |
||||
|
widgetsTitleList = this.aliasToWidgetsMap[datasource.entityAliasId]; |
||||
|
if (!widgetsTitleList) { |
||||
|
widgetsTitleList = []; |
||||
|
this.aliasToWidgetsMap[datasource.entityAliasId] = widgetsTitleList; |
||||
|
} |
||||
|
widgetsTitleList.push(widget.config.title); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
const entityAliasControls: Array<AbstractControl> = []; |
||||
|
for (const aliasId of Object.keys(this.data.entityAliases)) { |
||||
|
const entityAlias = this.data.entityAliases[aliasId]; |
||||
|
let filter = entityAlias.filter; |
||||
|
if (!filter) { |
||||
|
filter = { |
||||
|
resolveMultiple: false |
||||
|
}; |
||||
|
} |
||||
|
if (!filter.resolveMultiple) { |
||||
|
filter.resolveMultiple = false; |
||||
|
} |
||||
|
entityAliasControls.push(this.createEntityAliasFormControl(aliasId, entityAlias)); |
||||
|
} |
||||
|
|
||||
|
this.entityAliasesFormGroup = this.fb.group({ |
||||
|
entityAliases: this.fb.array(entityAliasControls) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private createEntityAliasFormControl(aliasId: string, entityAlias: EntityAlias): AbstractControl { |
||||
|
const aliasFormControl = this.fb.group({ |
||||
|
id: [aliasId], |
||||
|
alias: [entityAlias ? entityAlias.alias : null, [Validators.required]], |
||||
|
filter: [entityAlias ? entityAlias.filter : null], |
||||
|
resolveMultiple: [entityAlias ? entityAlias.filter.resolveMultiple : false] |
||||
|
}); |
||||
|
aliasFormControl.get('resolveMultiple').valueChanges.subscribe((resolveMultiple: boolean) => { |
||||
|
(aliasFormControl.get('filter').value as EntityAliasFilter).resolveMultiple = resolveMultiple; |
||||
|
}); |
||||
|
return aliasFormControl; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
removeAlias(index: number) { |
||||
|
const entityAlias = (this.entityAliasesFormGroup.get('entityAliases').value as any[])[index]; |
||||
|
const widgetsTitleList = this.aliasToWidgetsMap[entityAlias.id]; |
||||
|
if (widgetsTitleList) { |
||||
|
let widgetsListHtml = ''; |
||||
|
for (const widgetTitle of widgetsTitleList) { |
||||
|
widgetsListHtml += '<br/>\'' + widgetTitle + '\''; |
||||
|
} |
||||
|
const message = this.translate.instant('entity.unable-delete-entity-alias-text', |
||||
|
{entityAlias: entityAlias.alias, widgetsList: widgetsListHtml}); |
||||
|
this.dialogs.alert(this.translate.instant('entity.unable-delete-entity-alias-title'), |
||||
|
message, this.translate.instant('action.close'), true); |
||||
|
} else { |
||||
|
(this.entityAliasesFormGroup.get('entityAliases') as FormArray).removeAt(index); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public addAlias() { |
||||
|
this.openAliasDialog(-1); |
||||
|
} |
||||
|
|
||||
|
public editAlias(index: number) { |
||||
|
this.openAliasDialog(index); |
||||
|
} |
||||
|
|
||||
|
private openAliasDialog(index: number) { |
||||
|
const isAdd = index === -1; |
||||
|
let alias; |
||||
|
const aliasesArray = this.entityAliasesFormGroup.get('entityAliases').value as any[]; |
||||
|
if (!isAdd) { |
||||
|
alias = aliasesArray[index]; |
||||
|
} |
||||
|
this.dialog.open<EntityAliasDialogComponent, EntityAliasDialogData, |
||||
|
EntityAlias>(EntityAliasDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
isAdd, |
||||
|
allowedEntityTypes: this.allowedEntityTypes, |
||||
|
entityAliases: aliasesArray, |
||||
|
alias: isAdd ? null : deepClone(alias) |
||||
|
} |
||||
|
}).afterClosed().subscribe((entityAlias) => { |
||||
|
if (entityAlias) { |
||||
|
if (isAdd) { |
||||
|
(this.entityAliasesFormGroup.get('entityAliases') as FormArray) |
||||
|
.push(this.createEntityAliasFormControl(entityAlias.id, entityAlias)); |
||||
|
} else { |
||||
|
const aliasFormControl = (this.entityAliasesFormGroup.get('entityAliases') as FormArray).at(index); |
||||
|
aliasFormControl.get('alias').patchValue(entityAlias.alias); |
||||
|
aliasFormControl.get('filter').patchValue(entityAlias.filter); |
||||
|
aliasFormControl.get('resolveMultiple').patchValue(entityAlias.filter.resolveMultiple); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.submitted = true; |
||||
|
const entityAliases: EntityAliases = {}; |
||||
|
const uniqueAliasList: {[alias: string]: string} = {}; |
||||
|
|
||||
|
let valid = true; |
||||
|
let message: string; |
||||
|
|
||||
|
const aliasesArray = this.entityAliasesFormGroup.get('entityAliases').value as any[]; |
||||
|
for (const aliasValue of aliasesArray) { |
||||
|
const aliasId: string = aliasValue.id; |
||||
|
const alias: string = aliasValue.alias; |
||||
|
const filter: EntityAliasFilter = aliasValue.filter; |
||||
|
if (uniqueAliasList[alias]) { |
||||
|
valid = false; |
||||
|
message = this.translate.instant('entity.duplicate-alias-error', {alias}); |
||||
|
break; |
||||
|
} else if (!filter || !filter.type) { |
||||
|
valid = false; |
||||
|
message = this.translate.instant('entity.missing-entity-filter-error', {alias}); |
||||
|
break; |
||||
|
} else { |
||||
|
uniqueAliasList[alias] = alias; |
||||
|
entityAliases[aliasId] = {id: aliasId, alias, filter}; |
||||
|
} |
||||
|
} |
||||
|
if (valid) { |
||||
|
this.dialogRef.close(entityAliases); |
||||
|
} else { |
||||
|
this.store.dispatch(new ActionNotificationShow( |
||||
|
{ |
||||
|
message, |
||||
|
type: 'error' |
||||
|
})); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 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" class="tb-entity-filter-view"> |
||||
|
<div *ngIf="!filter || !filter.type; else filterView" class="entity-filter-empty" translate>alias.no-entity-filter-specified</div> |
||||
|
<ng-template #filterView> |
||||
|
<div fxLayout="column"> |
||||
|
<div class="entity-filter-value">{{ filterDisplayValue }}</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</div> |
||||
@ -0,0 +1,36 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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-entity-filter-view { |
||||
|
.entity-filter-empty { |
||||
|
font-size: 14px; |
||||
|
line-height: 16px; |
||||
|
color: rgba(221, 44, 0, .87); |
||||
|
} |
||||
|
|
||||
|
.entity-filter-type { |
||||
|
font-size: 14px; |
||||
|
line-height: 16px; |
||||
|
color: rgba(0, 0, 0, .570588); |
||||
|
} |
||||
|
|
||||
|
.entity-filter-value { |
||||
|
font-size: 14px; |
||||
|
line-height: 16px; |
||||
|
color: rgba(0, 0, 0, .570588); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,230 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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 } from '@angular/core'; |
||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { AliasFilterType, EntityAliasFilter } from '@shared/models/alias.models'; |
||||
|
import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entity-filter-view', |
||||
|
templateUrl: './entity-filter-view.component.html', |
||||
|
styleUrls: ['./entity-filter-view.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => EntityFilterViewComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class EntityFilterViewComponent implements ControlValueAccessor { |
||||
|
|
||||
|
constructor(private translate: TranslateService) {} |
||||
|
|
||||
|
filterDisplayValue: string; |
||||
|
filter: EntityAliasFilter; |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
} |
||||
|
|
||||
|
writeValue(filter: EntityAliasFilter): void { |
||||
|
this.filter = filter; |
||||
|
if (this.filter && this.filter.type) { |
||||
|
let entityType: EntityType | AliasEntityType; |
||||
|
let prefix: string; |
||||
|
let allEntitiesText; |
||||
|
let anyRelationText; |
||||
|
let relationTypeText; |
||||
|
let rootEntityText; |
||||
|
let directionText; |
||||
|
switch (this.filter.type) { |
||||
|
case AliasFilterType.singleEntity: |
||||
|
entityType = this.filter.singleEntity.entityType; |
||||
|
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).list, |
||||
|
{count: 1}); |
||||
|
break; |
||||
|
case AliasFilterType.entityList: |
||||
|
entityType = this.filter.entityType; |
||||
|
const count = this.filter.entityList.length; |
||||
|
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).list, |
||||
|
{count}); |
||||
|
break; |
||||
|
case AliasFilterType.entityName: |
||||
|
entityType = this.filter.entityType; |
||||
|
prefix = this.filter.entityNameFilter; |
||||
|
this.filterDisplayValue = this.translate.instant(entityTypeTranslations.get(entityType).nameStartsWith, |
||||
|
{prefix}); |
||||
|
break; |
||||
|
case AliasFilterType.stateEntity: |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-state-entity-description'); |
||||
|
break; |
||||
|
case AliasFilterType.assetType: |
||||
|
const assetType = this.filter.assetType; |
||||
|
prefix = this.filter.assetNameFilter; |
||||
|
if (prefix && prefix.length) { |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-and-name-description', |
||||
|
{assetType, prefix}); |
||||
|
} else { |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-type-description', |
||||
|
{assetType}); |
||||
|
} |
||||
|
break; |
||||
|
case AliasFilterType.deviceType: |
||||
|
const deviceType = this.filter.deviceType; |
||||
|
prefix = this.filter.deviceNameFilter; |
||||
|
if (prefix && prefix.length) { |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-and-name-description', |
||||
|
{deviceType, prefix}); |
||||
|
} else { |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-type-description', |
||||
|
{deviceType}); |
||||
|
} |
||||
|
break; |
||||
|
case AliasFilterType.entityViewType: |
||||
|
const entityViewType = this.filter.entityViewType; |
||||
|
prefix = this.filter.entityViewNameFilter; |
||||
|
if (prefix && prefix.length) { |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-and-name-description', |
||||
|
{entityViewType, prefix}); |
||||
|
} else { |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-type-description', |
||||
|
{entityViewType}); |
||||
|
} |
||||
|
break; |
||||
|
case AliasFilterType.relationsQuery: |
||||
|
allEntitiesText = this.translate.instant('alias.all-entities'); |
||||
|
anyRelationText = this.translate.instant('alias.any-relation'); |
||||
|
if (this.filter.rootStateEntity) { |
||||
|
rootEntityText = this.translate.instant('alias.state-entity'); |
||||
|
} else { |
||||
|
rootEntityText = this.translate.instant(entityTypeTranslations.get(this.filter.rootEntity.entityType).type); |
||||
|
} |
||||
|
directionText = this.translate.instant('relation.direction-type.' + this.filter.direction); |
||||
|
const relationFilters = this.filter.filters; |
||||
|
if (relationFilters && relationFilters.length) { |
||||
|
const relationFiltersDisplayValues = []; |
||||
|
relationFilters.forEach((relationFilter) => { |
||||
|
let entitiesText; |
||||
|
if (relationFilter.entityTypes && relationFilter.entityTypes.length) { |
||||
|
const entitiesNamesList = []; |
||||
|
relationFilter.entityTypes.forEach((filterEntityType) => { |
||||
|
entitiesNamesList.push( |
||||
|
this.translate.instant(entityTypeTranslations.get(filterEntityType).typePlural) |
||||
|
); |
||||
|
}); |
||||
|
entitiesText = entitiesNamesList.join(', '); |
||||
|
} else { |
||||
|
entitiesText = allEntitiesText; |
||||
|
} |
||||
|
if (relationFilter.relationType && relationFilter.relationType.length) { |
||||
|
relationTypeText = `'${relationFilter.relationType}'`; |
||||
|
} else { |
||||
|
relationTypeText = anyRelationText; |
||||
|
} |
||||
|
const relationFilterDisplayValue = this.translate.instant('alias.filter-type-relations-query-description', |
||||
|
{ |
||||
|
entities: entitiesText, |
||||
|
relationType: relationTypeText, |
||||
|
direction: directionText, |
||||
|
rootEntity: rootEntityText |
||||
|
} |
||||
|
); |
||||
|
relationFiltersDisplayValues.push(relationFilterDisplayValue); |
||||
|
}); |
||||
|
this.filterDisplayValue = relationFiltersDisplayValues.join(', '); |
||||
|
} else { |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-relations-query-description', |
||||
|
{ |
||||
|
entities: allEntitiesText, |
||||
|
relationType: anyRelationText, |
||||
|
direction: directionText, |
||||
|
rootEntity: rootEntityText |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
break; |
||||
|
case AliasFilterType.assetSearchQuery: |
||||
|
case AliasFilterType.deviceSearchQuery: |
||||
|
case AliasFilterType.entityViewSearchQuery: |
||||
|
allEntitiesText = this.translate.instant('alias.all-entities'); |
||||
|
anyRelationText = this.translate.instant('alias.any-relation'); |
||||
|
if (this.filter.rootStateEntity) { |
||||
|
rootEntityText = this.translate.instant('alias.state-entity'); |
||||
|
} else { |
||||
|
rootEntityText = this.translate.instant(entityTypeTranslations.get(this.filter.rootEntity.entityType).type); |
||||
|
} |
||||
|
directionText = this.translate.instant('relation.direction-type.' + this.filter.direction); |
||||
|
if (this.filter.relationType && this.filter.relationType.length) { |
||||
|
relationTypeText = `'${filter.relationType}'`; |
||||
|
} else { |
||||
|
relationTypeText = anyRelationText; |
||||
|
} |
||||
|
|
||||
|
const translationValues: any = { |
||||
|
relationType: relationTypeText, |
||||
|
direction: directionText, |
||||
|
rootEntity: rootEntityText |
||||
|
}; |
||||
|
|
||||
|
if (this.filter.type === AliasFilterType.assetSearchQuery) { |
||||
|
const assetTypesQuoted = []; |
||||
|
this.filter.assetTypes.forEach((filterAssetType) => { |
||||
|
assetTypesQuoted.push(`'${filterAssetType}'`); |
||||
|
}); |
||||
|
const assetTypesText = assetTypesQuoted.join(', '); |
||||
|
translationValues.assetTypes = assetTypesText; |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-asset-search-query-description', |
||||
|
translationValues |
||||
|
); |
||||
|
} else if (this.filter.type === AliasFilterType.deviceSearchQuery) { |
||||
|
const deviceTypesQuoted = []; |
||||
|
this.filter.deviceTypes.forEach((filterDeviceType) => { |
||||
|
deviceTypesQuoted.push(`'${filterDeviceType}'`); |
||||
|
}); |
||||
|
const deviceTypesText = deviceTypesQuoted.join(', '); |
||||
|
translationValues.deviceTypes = deviceTypesText; |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-device-search-query-description', |
||||
|
translationValues |
||||
|
); |
||||
|
} else if (this.filter.type === AliasFilterType.entityViewSearchQuery) { |
||||
|
const entityViewTypesQuoted = []; |
||||
|
this.filter.entityViewTypes.forEach((filterEntityViewType) => { |
||||
|
entityViewTypesQuoted.push(`'${filterEntityViewType}'`); |
||||
|
}); |
||||
|
const entityViewTypesText = entityViewTypesQuoted.join(', '); |
||||
|
translationValues.entityViewTypes = entityViewTypesText; |
||||
|
this.filterDisplayValue = this.translate.instant('alias.filter-type-entity-view-search-query-description', |
||||
|
translationValues |
||||
|
); |
||||
|
} |
||||
|
break; |
||||
|
default: |
||||
|
this.filterDisplayValue = this.filter.type; |
||||
|
break; |
||||
|
} |
||||
|
} else { |
||||
|
this.filterDisplayValue = ''; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,244 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 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]="entityFilterFormGroup" class="tb-entity-filter"> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>alias.filter-type</mat-label> |
||||
|
<mat-select required matInput formControlName="type"> |
||||
|
<mat-option *ngFor="let type of aliasFilterTypes" [value]="type"> |
||||
|
{{aliasFilterTypeTranslations.get(type) | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="entityFilterFormGroup.get('type').hasError('required')"> |
||||
|
{{ 'alias.filter-type-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<section fxLayout="column" [formGroup]="filterFormGroup" [ngSwitch]="entityFilterFormGroup.get('type').value"> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.singleEntity"> |
||||
|
<tb-entity-select required |
||||
|
useAliasEntityTypes="true" |
||||
|
formControlName="singleEntity"> |
||||
|
</tb-entity-select> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.entityList"> |
||||
|
<tb-entity-type-select required |
||||
|
showLabel |
||||
|
[allowedEntityTypes]="allowedEntityTypes" |
||||
|
formControlName="entityType"> |
||||
|
</tb-entity-type-select> |
||||
|
<tb-entity-list required |
||||
|
[entityType]="filterFormGroup.get('entityType').value" |
||||
|
formControlName="entityList"> |
||||
|
</tb-entity-list> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.entityName"> |
||||
|
<tb-entity-type-select required |
||||
|
showLabel |
||||
|
[allowedEntityTypes]="allowedEntityTypes" |
||||
|
formControlName="entityType"> |
||||
|
</tb-entity-type-select> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>entity.name-starts-with</mat-label> |
||||
|
<input matInput formControlName="entityNameFilter" required> |
||||
|
<mat-error *ngIf="filterFormGroup.get('entityNameFilter').hasError('required')"> |
||||
|
{{ 'entity.entity-name-filter-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.stateEntity"> |
||||
|
<mat-form-field floatLabel="always" class="mat-block"> |
||||
|
<mat-label translate>alias.state-entity-parameter-name</mat-label> |
||||
|
<input matInput formControlName="stateEntityParamName" |
||||
|
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<div fxLayout="column"> |
||||
|
<label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label> |
||||
|
<tb-entity-select fxFlex |
||||
|
useAliasEntityTypes="true" |
||||
|
formControlName="defaultStateEntity"> |
||||
|
</tb-entity-select> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.assetType"> |
||||
|
<tb-entity-subtype-autocomplete required |
||||
|
formControlName="assetType" |
||||
|
[entityType]="entityType.ASSET"> |
||||
|
</tb-entity-subtype-autocomplete> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>asset.name-starts-with</mat-label> |
||||
|
<input matInput formControlName="assetNameFilter"> |
||||
|
</mat-form-field> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.deviceType"> |
||||
|
<tb-entity-subtype-autocomplete required |
||||
|
formControlName="deviceType" |
||||
|
[entityType]="entityType.DEVICE"> |
||||
|
</tb-entity-subtype-autocomplete> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>device.name-starts-with</mat-label> |
||||
|
<input matInput formControlName="deviceNameFilter"> |
||||
|
</mat-form-field> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.entityViewType"> |
||||
|
<tb-entity-subtype-autocomplete required |
||||
|
formControlName="entityViewType" |
||||
|
[entityType]="entityType.ENTITY_VIEW"> |
||||
|
</tb-entity-subtype-autocomplete> |
||||
|
<mat-form-field class="mat-block"> |
||||
|
<mat-label translate>entity-view.name-starts-with</mat-label> |
||||
|
<input matInput formControlName="entityViewNameFilter"> |
||||
|
</mat-form-field> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.relationsQuery"> |
||||
|
<section fxLayout="column" id="relationsQueryFilter"> |
||||
|
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label> |
||||
|
<section class="tb-root-state-entity-switch" fxLayout="row" fxLayoutAlign="start center" style="padding-left: 0px;"> |
||||
|
<mat-slide-toggle class="root-state-entity-switch" |
||||
|
formControlName="rootStateEntity"> |
||||
|
</mat-slide-toggle> |
||||
|
<label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label> |
||||
|
</section> |
||||
|
<div fxFlex fxLayout="row" *ngIf="!filterFormGroup.get('rootStateEntity').value"> |
||||
|
<tb-entity-select fxFlex |
||||
|
required |
||||
|
useAliasEntityTypes="true" |
||||
|
formControlName="rootEntity"> |
||||
|
</tb-entity-select> |
||||
|
</div> |
||||
|
<div fxFlex fxLayout="row" *ngIf="filterFormGroup.get('rootStateEntity').value"> |
||||
|
<mat-form-field floatLabel="always" class="mat-block" style="margin-top: 14px; padding-right: 8px;"> |
||||
|
<mat-label translate>alias.state-entity-parameter-name</mat-label> |
||||
|
<input matInput formControlName="stateEntityParamName" |
||||
|
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<div fxFlex fxLayout="column"> |
||||
|
<label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label> |
||||
|
<tb-entity-select fxFlex |
||||
|
useAliasEntityTypes="true" |
||||
|
formControlName="defaultStateEntity"> |
||||
|
</tb-entity-select> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxFlex fxLayoutGap="8px" fxLayout="row"> |
||||
|
<mat-form-field class="mat-block" style="min-width: 100px;"> |
||||
|
<mat-label translate>relation.direction</mat-label> |
||||
|
<mat-select required matInput formControlName="direction"> |
||||
|
<mat-option *ngFor="let type of directionTypes" [value]="type"> |
||||
|
{{ directionTypeTranslations.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex floatLabel="always" class="mat-block"> |
||||
|
<mat-label translate>alias.max-relation-level</mat-label> |
||||
|
<input matInput |
||||
|
type="number" |
||||
|
min="1" |
||||
|
step="1" |
||||
|
placeholder="{{ 'alias.unlimited-level' | translate }}" |
||||
|
formControlName="maxLevel"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="mat-caption" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>relation.relation-filters</div> |
||||
|
<tb-relation-filters |
||||
|
[allowedEntityTypes]="allowedEntityTypes" |
||||
|
formControlName="filters"> |
||||
|
</tb-relation-filters> |
||||
|
</section> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="entityFilterFormGroup.get('type').value === aliasFilterType.assetSearchQuery || |
||||
|
entityFilterFormGroup.get('type').value === aliasFilterType.deviceSearchQuery || |
||||
|
entityFilterFormGroup.get('type').value === aliasFilterType.entityViewSearchQuery ? |
||||
|
entityFilterFormGroup.get('type').value : ''"> |
||||
|
<label class="tb-small">{{ 'alias.root-entity' | translate }}</label> |
||||
|
<section class="tb-root-state-entity-switch" fxLayout="row" fxLayoutAlign="start center" style="padding-left: 0px;"> |
||||
|
<mat-slide-toggle class="root-state-entity-switch" |
||||
|
formControlName="rootStateEntity"> |
||||
|
</mat-slide-toggle> |
||||
|
<label class="tb-small root-state-entity-label" translate>alias.root-state-entity</label> |
||||
|
</section> |
||||
|
<div fxFlex fxLayout="row" *ngIf="!filterFormGroup.get('rootStateEntity').value"> |
||||
|
<tb-entity-select fxFlex |
||||
|
required |
||||
|
useAliasEntityTypes="true" |
||||
|
formControlName="rootEntity"> |
||||
|
</tb-entity-select> |
||||
|
</div> |
||||
|
<div fxFlex fxLayout="row" *ngIf="filterFormGroup.get('rootStateEntity').value"> |
||||
|
<mat-form-field floatLabel="always" class="mat-block" style="margin-top: 14px; padding-right: 8px;"> |
||||
|
<mat-label translate>alias.state-entity-parameter-name</mat-label> |
||||
|
<input matInput formControlName="stateEntityParamName" |
||||
|
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<div fxFlex fxLayout="column"> |
||||
|
<label class="tb-small">{{ 'alias.default-state-entity' | translate }}</label> |
||||
|
<tb-entity-select fxFlex |
||||
|
useAliasEntityTypes="true" |
||||
|
formControlName="defaultStateEntity"> |
||||
|
</tb-entity-select> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div fxFlex fxLayoutGap="8px" fxLayout="row"> |
||||
|
<mat-form-field class="mat-block" style="min-width: 100px;"> |
||||
|
<mat-label translate>relation.direction</mat-label> |
||||
|
<mat-select required matInput formControlName="direction"> |
||||
|
<mat-option *ngFor="let type of directionTypes" [value]="type"> |
||||
|
{{ directionTypeTranslations.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex floatLabel="always" class="mat-block"> |
||||
|
<mat-label translate>alias.max-relation-level</mat-label> |
||||
|
<input matInput |
||||
|
type="number" |
||||
|
min="1" |
||||
|
step="1" |
||||
|
placeholder="{{ 'alias.unlimited-level' | translate }}" |
||||
|
formControlName="maxLevel"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="mat-caption" style="color: rgba(0,0,0,0.57);" translate>relation.relation-type</div> |
||||
|
<tb-relation-type-autocomplete |
||||
|
fxFlex |
||||
|
formControlName="relationType"> |
||||
|
</tb-relation-type-autocomplete> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.assetSearchQuery"> |
||||
|
<div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>asset.asset-types</div> |
||||
|
<tb-entity-subtype-list |
||||
|
required |
||||
|
[entityType]="entityType.ASSET" |
||||
|
formControlName="assetTypes"> |
||||
|
</tb-entity-subtype-list> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.deviceSearchQuery"> |
||||
|
<div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>device.device-types</div> |
||||
|
<tb-entity-subtype-list |
||||
|
required |
||||
|
[entityType]="entityType.DEVICE" |
||||
|
formControlName="deviceTypes"> |
||||
|
</tb-entity-subtype-list> |
||||
|
</ng-template> |
||||
|
<ng-template [ngSwitchCase]="aliasFilterType.entityViewSearchQuery"> |
||||
|
<div class="mat-caption tb-required" style="color: rgba(0,0,0,0.57);" translate>entity-view.entity-view-types</div> |
||||
|
<tb-entity-subtype-list |
||||
|
required |
||||
|
[entityType]="entityType.ENTITY_VIEW" |
||||
|
formControlName="entityViewTypes"> |
||||
|
</tb-entity-subtype-list> |
||||
|
</ng-template> |
||||
|
</section> |
||||
|
</div> |
||||
@ -0,0 +1,38 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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-entity-filter { |
||||
|
#relationsQueryFilter { |
||||
|
padding-top: 20px; |
||||
|
|
||||
|
tb-entity-select { |
||||
|
min-height: 92px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-root-state-entity-switch { |
||||
|
padding-left: 10px; |
||||
|
|
||||
|
.root-state-entity-switch { |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.root-state-entity-label { |
||||
|
margin: 5px 0 5px 10px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,218 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, FormControl, |
||||
|
FormGroup, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidatorFn, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models'; |
||||
|
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { EntitySearchDirection, entitySearchDirectionTranslations } from '@shared/models/relation.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entity-filter', |
||||
|
templateUrl: './entity-filter.component.html', |
||||
|
styleUrls: ['./entity-filter.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => EntityFilterComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class EntityFilterComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() allowedEntityTypes: Array<EntityType | AliasEntityType>; |
||||
|
|
||||
|
@Input() resolveMultiple: boolean; |
||||
|
|
||||
|
@Output() resolveMultipleChanged: EventEmitter<boolean> = new EventEmitter<boolean>(); |
||||
|
|
||||
|
entityFilterFormGroup: FormGroup; |
||||
|
filterFormGroup: FormGroup; |
||||
|
|
||||
|
aliasFilterTypes: Array<AliasFilterType>; |
||||
|
|
||||
|
aliasFilterType = AliasFilterType; |
||||
|
aliasFilterTypeTranslations = aliasFilterTypeTranslationMap; |
||||
|
entityType = EntityType; |
||||
|
|
||||
|
directionTypes = Object.keys(EntitySearchDirection); |
||||
|
directionTypeTranslations = entitySearchDirectionTranslations; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(private translate: TranslateService, |
||||
|
private entityService: EntityService, |
||||
|
private fb: FormBuilder) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
|
||||
|
this.aliasFilterTypes = this.entityService.getAliasFilterTypesByEntityTypes(this.allowedEntityTypes); |
||||
|
|
||||
|
this.entityFilterFormGroup = this.fb.group({ |
||||
|
type: [null, [Validators.required]] |
||||
|
}); |
||||
|
this.entityFilterFormGroup.get('type').valueChanges.subscribe((type: AliasFilterType) => { |
||||
|
this.filterTypeChanged(type); |
||||
|
}); |
||||
|
this.entityFilterFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
this.filterFormGroup = this.fb.group({}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
|
||||
|
writeValue(filter: EntityAliasFilter): void { |
||||
|
if (!filter) { |
||||
|
filter = { |
||||
|
type: null, |
||||
|
resolveMultiple: this.resolveMultiple |
||||
|
}; |
||||
|
} |
||||
|
this.entityFilterFormGroup.get('type').patchValue(filter.type, {emitEvent: false}); |
||||
|
if (filter && filter.type) { |
||||
|
this.updateFilterFormGroup(filter.type, filter); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateFilterFormGroup(type: AliasFilterType, filter?: EntityAliasFilter) { |
||||
|
switch (type) { |
||||
|
case AliasFilterType.singleEntity: |
||||
|
this.filterFormGroup = this.fb.group({ |
||||
|
singleEntity: [filter ? filter.singleEntity : null, [Validators.required]] |
||||
|
}); |
||||
|
break; |
||||
|
case AliasFilterType.entityList: |
||||
|
this.filterFormGroup = this.fb.group({ |
||||
|
entityType: [filter ? filter.entityType : null, [Validators.required]], |
||||
|
entityList: [filter ? filter.entityList : [], [Validators.required]], |
||||
|
}); |
||||
|
break; |
||||
|
case AliasFilterType.entityName: |
||||
|
this.filterFormGroup = this.fb.group({ |
||||
|
entityType: [filter ? filter.entityType : null, [Validators.required]], |
||||
|
entityNameFilter: [filter ? filter.entityNameFilter : '', [Validators.required]], |
||||
|
}); |
||||
|
break; |
||||
|
case AliasFilterType.stateEntity: |
||||
|
this.filterFormGroup = this.fb.group({ |
||||
|
stateEntityParamName: [filter ? filter.stateEntityParamName : null, []], |
||||
|
defaultStateEntity: [filter ? filter.defaultStateEntity : null, []], |
||||
|
}); |
||||
|
break; |
||||
|
case AliasFilterType.assetType: |
||||
|
this.filterFormGroup = this.fb.group({ |
||||
|
assetType: [filter ? filter.assetType : null, [Validators.required]], |
||||
|
assetNameFilter: [filter ? filter.assetNameFilter : '', []], |
||||
|
}); |
||||
|
break; |
||||
|
case AliasFilterType.deviceType: |
||||
|
this.filterFormGroup = this.fb.group({ |
||||
|
deviceType: [filter ? filter.deviceType : null, [Validators.required]], |
||||
|
deviceNameFilter: [filter ? filter.deviceNameFilter : '', []], |
||||
|
}); |
||||
|
break; |
||||
|
case AliasFilterType.entityViewType: |
||||
|
this.filterFormGroup = this.fb.group({ |
||||
|
entityViewType: [filter ? filter.entityViewType : null, [Validators.required]], |
||||
|
entityViewNameFilter: [filter ? filter.entityViewNameFilter : '', []], |
||||
|
}); |
||||
|
break; |
||||
|
case AliasFilterType.relationsQuery: |
||||
|
case AliasFilterType.assetSearchQuery: |
||||
|
case AliasFilterType.deviceSearchQuery: |
||||
|
case AliasFilterType.entityViewSearchQuery: |
||||
|
this.filterFormGroup = this.fb.group({ |
||||
|
rootStateEntity: [filter ? filter.rootStateEntity : false, []], |
||||
|
stateEntityParamName: [filter ? filter.stateEntityParamName : null, []], |
||||
|
defaultStateEntity: [filter ? filter.defaultStateEntity : null, []], |
||||
|
rootEntity: [filter ? filter.rootEntity : null, (filter && filter.rootStateEntity) ? [] : [Validators.required]], |
||||
|
direction: [filter ? filter.direction : EntitySearchDirection.FROM, [Validators.required]], |
||||
|
maxLevel: [filter ? filter.maxLevel : 1, []], |
||||
|
}); |
||||
|
this.filterFormGroup.get('rootStateEntity').valueChanges.subscribe((rootStateEntity: boolean) => { |
||||
|
this.filterFormGroup.get('rootEntity').setValidators(rootStateEntity ? [] : [Validators.required]); |
||||
|
this.filterFormGroup.get('rootEntity').updateValueAndValidity(); |
||||
|
}); |
||||
|
if (type === AliasFilterType.relationsQuery) { |
||||
|
this.filterFormGroup.addControl('filters', |
||||
|
this.fb.control(filter ? filter.filters : [], [])); |
||||
|
} else { |
||||
|
this.filterFormGroup.addControl('relationType', |
||||
|
this.fb.control(filter ? filter.relationType : null, [])); |
||||
|
if (type === AliasFilterType.assetSearchQuery) { |
||||
|
this.filterFormGroup.addControl('assetTypes', |
||||
|
this.fb.control(filter ? filter.assetTypes : [], [Validators.required])); |
||||
|
} else if (type === AliasFilterType.deviceSearchQuery) { |
||||
|
this.filterFormGroup.addControl('deviceTypes', |
||||
|
this.fb.control(filter ? filter.deviceTypes : [], [Validators.required])); |
||||
|
} else if (type === AliasFilterType.entityViewSearchQuery) { |
||||
|
this.filterFormGroup.addControl('entityViewTypes', |
||||
|
this.fb.control(filter ? filter.entityViewTypes : [], [Validators.required])); |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
this.filterFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private filterTypeChanged(type: AliasFilterType) { |
||||
|
let resolveMultiple = true; |
||||
|
if (type === AliasFilterType.singleEntity || type === AliasFilterType.stateEntity) { |
||||
|
resolveMultiple = false; |
||||
|
} |
||||
|
if (this.resolveMultiple !== resolveMultiple) { |
||||
|
this.resolveMultipleChanged.emit(resolveMultiple); |
||||
|
} |
||||
|
this.updateFilterFormGroup(type); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
let filter = null; |
||||
|
if (this.entityFilterFormGroup.valid && this.filterFormGroup.valid) { |
||||
|
filter = { |
||||
|
type: this.entityFilterFormGroup.get('type').value, |
||||
|
resolveMultiple: this.resolveMultiple |
||||
|
}; |
||||
|
filter = {...filter, ...this.filterFormGroup.value}; |
||||
|
} |
||||
|
this.propagateChange(filter); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 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 class="tb-relation-filters" [formGroup]="relationFiltersFormGroup"> |
||||
|
<div class="header" [fxShow]="relationFiltersFormGroup.get('relationFilters').length"> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<span class="cell" style="width: 200px; min-width: 200px;" translate>relation.type</span> |
||||
|
<span class="cell" fxFlex translate>entity.entity-types</span> |
||||
|
<span class="cell" style="width: 40px; min-width: 40px;"> </span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="body" [fxShow]="relationFiltersFormGroup.get('relationFilters').length"> |
||||
|
<div class="row" fxFlex fxLayout="row" |
||||
|
fxLayoutAlign="start center" formArrayName="relationFilters" |
||||
|
*ngFor="let relationFilterControl of relationFiltersFormGroup.get('relationFilters').controls; let $index = index"> |
||||
|
<div class="mat-elevation-z1" fxFlex fxLayout="row" fxLayoutAlign="start center"> |
||||
|
<tb-relation-type-autocomplete |
||||
|
class="cell" style="width: 200px; min-width: 200px;" |
||||
|
[formControl]="relationFilterControl.get('relationType')"> |
||||
|
</tb-relation-type-autocomplete> |
||||
|
<tb-entity-type-list class="cell" fxFlex |
||||
|
[allowedEntityTypes]="allowedEntityTypes" |
||||
|
[formControl]="relationFilterControl.get('entityTypes')"> |
||||
|
</tb-entity-type-list> |
||||
|
<button mat-button mat-icon-button color="primary" |
||||
|
type="button" |
||||
|
style="width: 40px; min-width: 40px;" |
||||
|
(click)="removeFilter($index)" |
||||
|
[disabled]="isLoading$ | async" |
||||
|
matTooltip="{{ 'relation.remove-relation-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="any-filter" [fxShow]="!relationFiltersFormGroup.get('relationFilters').length"> |
||||
|
<span fxLayoutAlign="center center" |
||||
|
class="tb-prompt" translate>relation.any-relation</span> |
||||
|
</div> |
||||
|
<div> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
[disabled]="isLoading$ | async" |
||||
|
(click)="addFilter()" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'relation.add-relation-filter' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>add</mat-icon> |
||||
|
{{ 'action.add' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,67 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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-relation-filters { |
||||
|
.header { |
||||
|
padding-right: 5px; |
||||
|
padding-bottom: 5px; |
||||
|
padding-left: 5px; |
||||
|
|
||||
|
.cell { |
||||
|
padding-right: 5px; |
||||
|
padding-left: 5px; |
||||
|
font-size: 12px; |
||||
|
font-weight: 700; |
||||
|
color: rgba(0, 0, 0, .54); |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.body { |
||||
|
max-height: 300px; |
||||
|
padding-right: 5px; |
||||
|
padding-bottom: 20px; |
||||
|
padding-left: 5px; |
||||
|
overflow: auto; |
||||
|
|
||||
|
.row { |
||||
|
padding-top: 5px; |
||||
|
} |
||||
|
|
||||
|
.cell { |
||||
|
padding-right: 5px; |
||||
|
padding-left: 5px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.tb-relation-filters { |
||||
|
.cell { |
||||
|
.mat-form-field { |
||||
|
.mat-form-field-infix { |
||||
|
border-top: none; |
||||
|
} |
||||
|
&.mat-form-field-appearance-standard { |
||||
|
.mat-form-field-flex { |
||||
|
padding-top: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,123 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, FormArray, |
||||
|
FormBuilder, FormControl, |
||||
|
FormGroup, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidatorFn, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { AliasFilterType, aliasFilterTypeTranslationMap, EntityAliasFilter } from '@shared/models/alias.models'; |
||||
|
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { EntitySearchDirection, entitySearchDirectionTranslations, EntityTypeFilter } from '@shared/models/relation.models'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { Subscription } from 'rxjs'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-relation-filters', |
||||
|
templateUrl: './relation-filters.component.html', |
||||
|
styleUrls: ['./relation-filters.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => RelationFiltersComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class RelationFiltersComponent extends PageComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() allowedEntityTypes: Array<EntityType | AliasEntityType>; |
||||
|
|
||||
|
relationFiltersFormGroup: FormGroup; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
private valueChangeSubscription: Subscription = null; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.relationFiltersFormGroup = this.fb.group({}); |
||||
|
this.relationFiltersFormGroup.addControl('relationFilters', |
||||
|
this.fb.array([])); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
|
||||
|
writeValue(filters: Array<EntityTypeFilter>): void { |
||||
|
if (this.valueChangeSubscription) { |
||||
|
this.valueChangeSubscription.unsubscribe(); |
||||
|
} |
||||
|
const relationFiltersControls: Array<AbstractControl> = []; |
||||
|
if (filters && filters.length) { |
||||
|
filters.forEach((filter) => { |
||||
|
relationFiltersControls.push(this.createRelationFilterFormGroup(filter)); |
||||
|
}); |
||||
|
} |
||||
|
this.relationFiltersFormGroup.setControl('relationFilters', this.fb.array(relationFiltersControls)); |
||||
|
this.valueChangeSubscription = this.relationFiltersFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public removeFilter(index: number) { |
||||
|
(this.relationFiltersFormGroup.get('relationFilters') as FormArray).removeAt(index); |
||||
|
} |
||||
|
|
||||
|
public addFilter() { |
||||
|
const relationFiltersFormArray = this.relationFiltersFormGroup.get('relationFilters') as FormArray; |
||||
|
const filter: EntityTypeFilter = { |
||||
|
relationType: null, |
||||
|
entityTypes: [] |
||||
|
}; |
||||
|
relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter)); |
||||
|
} |
||||
|
|
||||
|
private createRelationFilterFormGroup(filter: EntityTypeFilter): AbstractControl { |
||||
|
return this.fb.group({ |
||||
|
relationType: [filter ? filter.relationType : null], |
||||
|
entityTypes: [filter ? filter.entityTypes : []] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const filters: Array<EntityTypeFilter> = this.relationFiltersFormGroup.get('relationFilters').value; |
||||
|
this.propagateChange(filters); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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 ::ng-deep { |
||||
|
.mat-form-field { |
||||
|
.mat-form-field-infix { |
||||
|
border-top: none; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 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 appearance="standard" [formGroup]="entitySubtypeListFormGroup" class="mat-block"> |
||||
|
<mat-chip-list #chipList formControlName="entitySubtypeList"> |
||||
|
<mat-chip |
||||
|
*ngFor="let entitySubtype of entitySubtypeList" |
||||
|
[selectable]="!disabled" |
||||
|
[removable]="!disabled" |
||||
|
(removed)="remove(entitySubtype)"> |
||||
|
{{entitySubtype}} |
||||
|
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> |
||||
|
</mat-chip> |
||||
|
<input matInput type="text" placeholder="{{ !disabled ? ((!entitySubtypeList || !entitySubtypeList.length) ? placeholder : secondaryPlaceholder) : '' }}" |
||||
|
style="max-width: 200px;" |
||||
|
#entitySubtypeInput |
||||
|
(focusin)="onFocus()" |
||||
|
formControlName="entitySubtype" |
||||
|
matAutocompleteOrigin |
||||
|
#origin="matAutocompleteOrigin" |
||||
|
[matAutocompleteConnectedTo]="origin" |
||||
|
[matAutocomplete]="entitySubtypeAutocomplete" |
||||
|
[matChipInputFor]="chipList" |
||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
||||
|
(matChipInputTokenEnd)="chipAdd($event)"> |
||||
|
</mat-chip-list> |
||||
|
<mat-autocomplete #entitySubtypeAutocomplete="matAutocomplete" |
||||
|
class="tb-autocomplete" |
||||
|
(optionSelected)="selected($event)" |
||||
|
[displayWith]="displayEntitySubtypeFn"> |
||||
|
<mat-option *ngFor="let entitySubtype of filteredEntitySubtypeList | async" [value]="entitySubtype"> |
||||
|
<span [innerHTML]="entitySubtype | highlight:searchText"></span> |
||||
|
</mat-option> |
||||
|
<mat-option *ngIf="!(filteredEntitySubtypeList | async)?.length" [value]="null"> |
||||
|
<span> |
||||
|
{{ translate.get(noSubtypesMathingText, {entitySubtype: searchText}) | async }} |
||||
|
</span> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
<mat-error *ngIf="entitySubtypeListFormGroup.get('entitySubtypeList').hasError('required')"> |
||||
|
{{ subtypeListEmptyText | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
@ -0,0 +1,22 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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 ::ng-deep { |
||||
|
.mat-form-field { |
||||
|
.mat-form-field-infix { |
||||
|
border-top: none; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,317 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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 { |
||||
|
AfterContentInit, |
||||
|
AfterViewInit, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
forwardRef, |
||||
|
Input, OnDestroy, |
||||
|
OnInit, |
||||
|
SkipSelf, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
FormControl, |
||||
|
FormGroup, |
||||
|
FormGroupDirective, |
||||
|
NG_VALUE_ACCESSOR, NgForm, Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Observable, of, Subscription, throwError } from 'rxjs'; |
||||
|
import { map, mergeMap, startWith, tap, share, pairwise, filter, publishReplay, refCount } from 'rxjs/operators'; |
||||
|
import {Store} from '@ngrx/store'; |
||||
|
import {AppState} from '@app/core/core.state'; |
||||
|
import {TranslateService} from '@ngx-translate/core'; |
||||
|
import { AliasEntityType, EntitySubtype, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; |
||||
|
import {BaseData} from '@shared/models/base-data'; |
||||
|
import {EntityId} from '@shared/models/id/entity-id'; |
||||
|
import {EntityService} from '@core/http/entity.service'; |
||||
|
import {ErrorStateMatcher, MatAutocomplete, MatAutocompleteSelectedEvent, MatChipList} from '@angular/material'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { emptyPageData } from '@shared/models/page/page-data'; |
||||
|
import { AssetService } from '@core/http/asset.service'; |
||||
|
import { DeviceService } from '@core/http/device.service'; |
||||
|
import { EntityViewService } from '@core/http/entity-view.service'; |
||||
|
import { BroadcastService } from '@core/services/broadcast.service'; |
||||
|
import { MatChipInputEvent } from '@angular/material/chips'; |
||||
|
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entity-subtype-list', |
||||
|
templateUrl: './entity-subtype-list.component.html', |
||||
|
styleUrls: ['./entity-subtype-list.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => EntitySubTypeListComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { |
||||
|
|
||||
|
entitySubtypeListFormGroup: FormGroup; |
||||
|
|
||||
|
modelValue: Array<string> | null; |
||||
|
|
||||
|
private requiredValue: boolean; |
||||
|
get required(): boolean { |
||||
|
return this.requiredValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set required(value: boolean) { |
||||
|
const newVal = coerceBooleanProperty(value); |
||||
|
if (this.requiredValue !== newVal) { |
||||
|
this.requiredValue = newVal; |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
entityType: EntityType; |
||||
|
|
||||
|
@ViewChild('entitySubtypeInput', {static: false}) entitySubtypeInput: ElementRef<HTMLInputElement>; |
||||
|
@ViewChild('entitySubtypeAutocomplete', {static: false}) entitySubtypeAutocomplete: MatAutocomplete; |
||||
|
@ViewChild('chipList', {static: true}) chipList: MatChipList; |
||||
|
|
||||
|
entitySubtypeList: Array<string> = []; |
||||
|
filteredEntitySubtypeList: Observable<Array<string>>; |
||||
|
entitySubtypes: Observable<Array<string>>; |
||||
|
|
||||
|
private broadcastSubscription: Subscription; |
||||
|
|
||||
|
placeholder: string; |
||||
|
secondaryPlaceholder: string; |
||||
|
noSubtypesMathingText: string; |
||||
|
subtypeListEmptyText: string; |
||||
|
|
||||
|
separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; |
||||
|
|
||||
|
private searchText = ''; |
||||
|
|
||||
|
private dirty = false; |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
private broadcast: BroadcastService, |
||||
|
public translate: TranslateService, |
||||
|
private assetService: AssetService, |
||||
|
private deviceService: DeviceService, |
||||
|
private entityViewService: EntityViewService, |
||||
|
private fb: FormBuilder) { |
||||
|
this.entitySubtypeListFormGroup = this.fb.group({ |
||||
|
entitySubtypeList: [this.entitySubtypeList, this.required ? [Validators.required] : []], |
||||
|
entitySubtype: [null] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
updateValidators() { |
||||
|
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValidators(this.required ? [Validators.required] : []); |
||||
|
this.entitySubtypeListFormGroup.get('entitySubtypeList').updateValueAndValidity(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
|
||||
|
switch (this.entityType) { |
||||
|
case EntityType.ASSET: |
||||
|
this.placeholder = this.required ? this.translate.instant('asset.enter-asset-type') |
||||
|
: this.translate.instant('asset.any-asset'); |
||||
|
this.secondaryPlaceholder = '+' + this.translate.instant('asset.asset-type'); |
||||
|
this.noSubtypesMathingText = 'asset.no-asset-types-matching'; |
||||
|
this.subtypeListEmptyText = 'asset.asset-type-list-empty'; |
||||
|
this.broadcastSubscription = this.broadcast.on('assetSaved', () => { |
||||
|
this.entitySubtypes = null; |
||||
|
}); |
||||
|
break; |
||||
|
case EntityType.DEVICE: |
||||
|
this.placeholder = this.required ? this.translate.instant('device.enter-device-type') |
||||
|
: this.translate.instant('device.any-device'); |
||||
|
this.secondaryPlaceholder = '+' + this.translate.instant('device.device-type'); |
||||
|
this.noSubtypesMathingText = 'device.no-device-types-matching'; |
||||
|
this.subtypeListEmptyText = 'device.device-type-list-empty'; |
||||
|
this.broadcastSubscription = this.broadcast.on('deviceSaved', () => { |
||||
|
this.entitySubtypes = null; |
||||
|
}); |
||||
|
break; |
||||
|
case EntityType.ENTITY_VIEW: |
||||
|
this.placeholder = this.required ? this.translate.instant('entity-view.enter-entity-view-type') |
||||
|
: this.translate.instant('entity-view.any-entity-view'); |
||||
|
this.secondaryPlaceholder = '+' + this.translate.instant('entity-view.entity-view-type'); |
||||
|
this.noSubtypesMathingText = 'entity-view.no-entity-view-types-matching'; |
||||
|
this.subtypeListEmptyText = 'entity-view.entity-view-type-list-empty'; |
||||
|
this.broadcastSubscription = this.broadcast.on('entityViewSaved', () => { |
||||
|
this.entitySubtypes = null; |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
this.filteredEntitySubtypeList = this.entitySubtypeListFormGroup.get('entitySubtype').valueChanges |
||||
|
.pipe( |
||||
|
map(value => value ? value : ''), |
||||
|
mergeMap(name => this.fetchEntitySubtypes(name) ), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
if (this.broadcastSubscription) { |
||||
|
this.broadcastSubscription.unsubscribe(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.entitySubtypeListFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.entitySubtypeListFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: Array<string> | null): void { |
||||
|
this.searchText = ''; |
||||
|
if (value != null && value.length > 0) { |
||||
|
this.modelValue = [...value]; |
||||
|
this.entitySubtypeList = [...value]; |
||||
|
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); |
||||
|
} else { |
||||
|
this.entitySubtypeList = []; |
||||
|
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); |
||||
|
this.modelValue = null; |
||||
|
} |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
|
||||
|
add(entitySubtype: string): void { |
||||
|
if (!this.modelValue || this.modelValue.indexOf(entitySubtype) === -1) { |
||||
|
if (!this.modelValue) { |
||||
|
this.modelValue = []; |
||||
|
} |
||||
|
this.modelValue.push(entitySubtype); |
||||
|
this.entitySubtypeList.push(entitySubtype); |
||||
|
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); |
||||
|
} |
||||
|
this.propagateChange(this.modelValue); |
||||
|
this.clear(); |
||||
|
} |
||||
|
|
||||
|
chipAdd(event: MatChipInputEvent): void { |
||||
|
const value = event.value; |
||||
|
if ((value || '').trim()) { |
||||
|
this.add(value.trim()); |
||||
|
} |
||||
|
this.clear(''); |
||||
|
} |
||||
|
|
||||
|
remove(entitySubtype: string) { |
||||
|
const index = this.entitySubtypeList.indexOf(entitySubtype); |
||||
|
if (index >= 0) { |
||||
|
this.entitySubtypeList.splice(index, 1); |
||||
|
this.entitySubtypeListFormGroup.get('entitySubtypeList').setValue(this.entitySubtypeList); |
||||
|
this.modelValue.splice(index, 1); |
||||
|
if (!this.modelValue.length) { |
||||
|
this.modelValue = null; |
||||
|
} |
||||
|
this.propagateChange(this.modelValue); |
||||
|
this.clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
selected(event: MatAutocompleteSelectedEvent): void { |
||||
|
this.add(event.option.viewValue); |
||||
|
this.clear(''); |
||||
|
} |
||||
|
|
||||
|
displayEntitySubtypeFn(entitySubtype?: string): string | undefined { |
||||
|
return entitySubtype ? entitySubtype : undefined; |
||||
|
} |
||||
|
|
||||
|
fetchEntitySubtypes(searchText?: string): Observable<Array<string>> { |
||||
|
this.searchText = searchText; |
||||
|
return this.getEntitySubtypes().pipe( |
||||
|
map(subTypes => { |
||||
|
let result = subTypes.filter( subType => { |
||||
|
return searchText ? subType.toUpperCase().startsWith(searchText.toUpperCase()) : true; |
||||
|
}); |
||||
|
if (!result.length) { |
||||
|
result = [searchText]; |
||||
|
} |
||||
|
return result; |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
getEntitySubtypes(): Observable<Array<string>> { |
||||
|
if (!this.entitySubtypes) { |
||||
|
let subTypesObservable: Observable<Array<EntitySubtype>>; |
||||
|
switch (this.entityType) { |
||||
|
case EntityType.ASSET: |
||||
|
subTypesObservable = this.assetService.getAssetTypes(false, true); |
||||
|
break; |
||||
|
case EntityType.DEVICE: |
||||
|
subTypesObservable = this.deviceService.getDeviceTypes(false, true); |
||||
|
break; |
||||
|
case EntityType.ENTITY_VIEW: |
||||
|
subTypesObservable = this.entityViewService.getEntityViewTypes(false, true); |
||||
|
break; |
||||
|
} |
||||
|
if (subTypesObservable) { |
||||
|
this.entitySubtypes = subTypesObservable.pipe( |
||||
|
map(subTypes => subTypes.map(subType => subType.type)), |
||||
|
publishReplay(1), |
||||
|
refCount() |
||||
|
); |
||||
|
} else { |
||||
|
return throwError(null); |
||||
|
} |
||||
|
} |
||||
|
return this.entitySubtypes; |
||||
|
} |
||||
|
|
||||
|
onFocus() { |
||||
|
if (this.dirty) { |
||||
|
this.entitySubtypeListFormGroup.get('entitySubtype').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
||||
|
this.dirty = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clear(value: string = '') { |
||||
|
this.entitySubtypeInput.nativeElement.value = value; |
||||
|
this.entitySubtypeListFormGroup.get('entitySubtype').patchValue(value, {emitEvent: true}); |
||||
|
setTimeout(() => { |
||||
|
this.entitySubtypeInput.nativeElement.blur(); |
||||
|
this.entitySubtypeInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 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 appearance="standard" [formGroup]="entityTypeListFormGroup" class="mat-block"> |
||||
|
<mat-chip-list #chipList formControlName="entityTypeList"> |
||||
|
<mat-chip |
||||
|
*ngFor="let entityType of entityTypeList" |
||||
|
[selectable]="!disabled" |
||||
|
[removable]="!disabled" |
||||
|
(removed)="remove(entityType)"> |
||||
|
{{entityType.name}} |
||||
|
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> |
||||
|
</mat-chip> |
||||
|
<input matInput type="text" placeholder="{{ !disabled ? ((!entityTypeList || !entityTypeList.length) ? placeholder : secondaryPlaceholder) : '' }}" |
||||
|
style="max-width: 200px;" |
||||
|
#entityTypeInput |
||||
|
(focusin)="onFocus()" |
||||
|
formControlName="entityType" |
||||
|
matAutocompleteOrigin |
||||
|
#origin="matAutocompleteOrigin" |
||||
|
[matAutocompleteConnectedTo]="origin" |
||||
|
[matAutocomplete]="entityTypeAutocomplete" |
||||
|
[matChipInputFor]="chipList"> |
||||
|
</mat-chip-list> |
||||
|
<mat-autocomplete #entityTypeAutocomplete="matAutocomplete" |
||||
|
class="tb-autocomplete" |
||||
|
[displayWith]="displayEntityTypeFn"> |
||||
|
<mat-option *ngFor="let entityType of filteredEntityTypeList | async" [value]="entityType"> |
||||
|
<span [innerHTML]="entityType.name | highlight:searchText"></span> |
||||
|
</mat-option> |
||||
|
<mat-option *ngIf="!(filteredEntityTypeList | async)?.length" [value]="null"> |
||||
|
<span> |
||||
|
{{ translate.get('entity.no-entity-types-matching', {entityType: searchText}) | async }} |
||||
|
</span> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
<mat-error *ngIf="entityTypeListFormGroup.get('entityTypeList').hasError('required')"> |
||||
|
{{ 'entity.entity-type-list-empty' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
@ -0,0 +1,262 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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 { |
||||
|
AfterContentInit, |
||||
|
AfterViewInit, |
||||
|
Component, |
||||
|
ElementRef, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
SkipSelf, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
FormControl, |
||||
|
FormGroup, |
||||
|
FormGroupDirective, |
||||
|
NG_VALUE_ACCESSOR, NgForm, Validators |
||||
|
} from '@angular/forms'; |
||||
|
import {Observable, of} from 'rxjs'; |
||||
|
import {map, mergeMap, startWith, tap, share, pairwise, filter} from 'rxjs/operators'; |
||||
|
import {Store} from '@ngrx/store'; |
||||
|
import {AppState} from '@app/core/core.state'; |
||||
|
import {TranslateService} from '@ngx-translate/core'; |
||||
|
import { AliasEntityType, EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; |
||||
|
import {BaseData} from '@shared/models/base-data'; |
||||
|
import {EntityId} from '@shared/models/id/entity-id'; |
||||
|
import {EntityService} from '@core/http/entity.service'; |
||||
|
import {ErrorStateMatcher, MatAutocomplete, MatAutocompleteSelectedEvent, MatChipList} from '@angular/material'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { emptyPageData } from '@shared/models/page/page-data'; |
||||
|
|
||||
|
interface EntityTypeInfo { |
||||
|
name: string; |
||||
|
value: EntityType; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entity-type-list', |
||||
|
templateUrl: './entity-type-list.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => EntityTypeListComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class EntityTypeListComponent implements ControlValueAccessor, OnInit, AfterViewInit { |
||||
|
|
||||
|
entityTypeListFormGroup: FormGroup; |
||||
|
|
||||
|
modelValue: Array<EntityType> | null; |
||||
|
|
||||
|
private requiredValue: boolean; |
||||
|
get required(): boolean { |
||||
|
return this.requiredValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set required(value: boolean) { |
||||
|
const newVal = coerceBooleanProperty(value); |
||||
|
if (this.requiredValue !== newVal) { |
||||
|
this.requiredValue = newVal; |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
allowedEntityTypes: Array<EntityType | AliasEntityType>; |
||||
|
|
||||
|
@Input() |
||||
|
ignoreAuthorityFilter: boolean; |
||||
|
|
||||
|
@ViewChild('entityTypeInput', {static: false}) entityTypeInput: ElementRef<HTMLInputElement>; |
||||
|
@ViewChild('entityTypeAutocomplete', {static: false}) entityTypeAutocomplete: MatAutocomplete; |
||||
|
@ViewChild('chipList', {static: true}) chipList: MatChipList; |
||||
|
|
||||
|
allEntityTypeList: Array<EntityTypeInfo> = []; |
||||
|
entityTypeList: Array<EntityTypeInfo> = []; |
||||
|
filteredEntityTypeList: Observable<Array<EntityTypeInfo>>; |
||||
|
|
||||
|
placeholder: string; |
||||
|
secondaryPlaceholder: string; |
||||
|
|
||||
|
private searchText = ''; |
||||
|
|
||||
|
private dirty = false; |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
public translate: TranslateService, |
||||
|
private entityService: EntityService, |
||||
|
private fb: FormBuilder) { |
||||
|
this.entityTypeListFormGroup = this.fb.group({ |
||||
|
entityTypeList: [this.entityTypeList, this.required ? [Validators.required] : []], |
||||
|
entityType: [null] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
updateValidators() { |
||||
|
this.entityTypeListFormGroup.get('entityTypeList').setValidators(this.required ? [Validators.required] : []); |
||||
|
this.entityTypeListFormGroup.get('entityTypeList').updateValueAndValidity(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
|
||||
|
this.placeholder = this.required ? this.translate.instant('entity.enter-entity-type') |
||||
|
: this.translate.instant('entity.any-entity'); |
||||
|
this.secondaryPlaceholder = '+' + this.translate.instant('entity.entity-type'); |
||||
|
|
||||
|
let entityTypes: Array<EntityType | AliasEntityType>; |
||||
|
if (this.ignoreAuthorityFilter && this.allowedEntityTypes |
||||
|
&& this.allowedEntityTypes.length) { |
||||
|
entityTypes = []; |
||||
|
this.allowedEntityTypes.forEach((entityTypeValue) => { |
||||
|
entityTypes.push(entityTypeValue); |
||||
|
}); |
||||
|
} else { |
||||
|
entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes) as Array<EntityType>; |
||||
|
} |
||||
|
|
||||
|
entityTypes.forEach((entityType) => { |
||||
|
this.allEntityTypeList.push({ |
||||
|
name: this.translate.instant(entityTypeTranslations.get(entityType).type), |
||||
|
value: entityType as EntityType |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
this.filteredEntityTypeList = this.entityTypeListFormGroup.get('entityType').valueChanges |
||||
|
.pipe( |
||||
|
// startWith<string | BaseData<EntityId>>(''),
|
||||
|
tap((value) => { |
||||
|
if (value && typeof value !== 'string') { |
||||
|
this.add(value); |
||||
|
} else if (value === null) { |
||||
|
this.clear(this.entityTypeInput.nativeElement.value); |
||||
|
} |
||||
|
}), |
||||
|
filter((value) => typeof value === 'string'), |
||||
|
map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), |
||||
|
mergeMap(name => this.fetchEntityTypes(name) ), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.entityTypeListFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.entityTypeListFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: Array<EntityType> | null): void { |
||||
|
this.searchText = ''; |
||||
|
if (value != null && value.length > 0) { |
||||
|
this.modelValue = [...value]; |
||||
|
this.entityTypeList = []; |
||||
|
value.forEach((entityType) => { |
||||
|
this.entityTypeList.push({ |
||||
|
name: this.translate.instant(entityTypeTranslations.get(entityType).type), |
||||
|
value: entityType |
||||
|
}); |
||||
|
}); |
||||
|
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); |
||||
|
} else { |
||||
|
this.entityTypeList = []; |
||||
|
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); |
||||
|
this.modelValue = null; |
||||
|
} |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
|
||||
|
add(entityType: EntityTypeInfo): void { |
||||
|
if (!this.modelValue || this.modelValue.indexOf(entityType.value) === -1) { |
||||
|
if (!this.modelValue) { |
||||
|
this.modelValue = []; |
||||
|
} |
||||
|
this.modelValue.push(entityType.value); |
||||
|
this.entityTypeList.push(entityType); |
||||
|
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); |
||||
|
} |
||||
|
this.propagateChange(this.modelValue); |
||||
|
this.clear(); |
||||
|
} |
||||
|
|
||||
|
remove(entityType: EntityTypeInfo) { |
||||
|
const index = this.entityTypeList.indexOf(entityType); |
||||
|
if (index >= 0) { |
||||
|
this.entityTypeList.splice(index, 1); |
||||
|
this.entityTypeListFormGroup.get('entityTypeList').setValue(this.entityTypeList); |
||||
|
this.modelValue.splice(index, 1); |
||||
|
if (!this.modelValue.length) { |
||||
|
this.modelValue = null; |
||||
|
} |
||||
|
this.propagateChange(this.modelValue); |
||||
|
this.clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayEntityTypeFn(entityType?: EntityTypeInfo): string | undefined { |
||||
|
return entityType ? entityType.name : undefined; |
||||
|
} |
||||
|
|
||||
|
fetchEntityTypes(searchText?: string): Observable<Array<EntityTypeInfo>> { |
||||
|
this.searchText = searchText; |
||||
|
let result = this.allEntityTypeList; |
||||
|
if (searchText && searchText.length) { |
||||
|
result = this.allEntityTypeList.filter((entityTypeInfo) => entityTypeInfo.name.toLowerCase().includes(searchText.toLowerCase())); |
||||
|
} |
||||
|
return of(result); |
||||
|
} |
||||
|
|
||||
|
onFocus() { |
||||
|
if (this.dirty) { |
||||
|
this.entityTypeListFormGroup.get('entityType').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
||||
|
this.dirty = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clear(value: string = '') { |
||||
|
this.entityTypeInput.nativeElement.value = value; |
||||
|
this.entityTypeListFormGroup.get('entityType').patchValue(value, {emitEvent: true}); |
||||
|
setTimeout(() => { |
||||
|
this.entityTypeInput.nativeElement.blur(); |
||||
|
this.entityTypeInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2019 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, Input } from '@angular/core'; |
||||
|
import { trigger, state, style, transition, animate } from '@angular/animations'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-error', |
||||
|
template: ` |
||||
|
<div [@animation]="state" style="margin-top:0.5rem;font-size:.75rem"> |
||||
|
<mat-error > |
||||
|
{{message}} |
||||
|
</mat-error> |
||||
|
</div> |
||||
|
`,
|
||||
|
styles: [` |
||||
|
:host { |
||||
|
height: 24px; |
||||
|
} |
||||
|
`],
|
||||
|
animations: [ |
||||
|
trigger('animation', [ |
||||
|
state('show', style({ |
||||
|
opacity: 1, |
||||
|
})), |
||||
|
state('hide', style({ |
||||
|
opacity: 0, |
||||
|
transform: 'translateY(-1rem)' |
||||
|
})), |
||||
|
transition('show => hide', animate('200ms ease-out')), |
||||
|
transition('* => show', animate('200ms ease-in')) |
||||
|
|
||||
|
]), |
||||
|
] |
||||
|
}) |
||||
|
export class TbErrorComponent { |
||||
|
errorValue: any; |
||||
|
state: any; |
||||
|
message; |
||||
|
|
||||
|
@Input() |
||||
|
set error(value) { |
||||
|
if (value && !this.message) { |
||||
|
this.message = value; |
||||
|
this.state = 'hide'; |
||||
|
setTimeout(() => { |
||||
|
this.state = 'show'; |
||||
|
}); |
||||
|
} else { |
||||
|
this.errorValue = value; |
||||
|
this.state = value ? 'show' : 'hide'; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue