27 changed files with 1195 additions and 77 deletions
@ -0,0 +1,60 @@ |
|||
<!-- |
|||
|
|||
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 floatLabel="always" hideRequiredMarker [formGroup]="selectEntityAliasFormGroup" class="mat-block"> |
|||
<mat-label></mat-label> |
|||
<input matInput type="text" placeholder="{{ 'entity.entity-alias' | translate }}" |
|||
#entityAliasInput |
|||
formControlName="entityAlias" |
|||
(focusin)="onFocus()" |
|||
[required]="required" |
|||
(keydown)="entityAliasEnter($event)" |
|||
(keypress)="entityAliasEnter($event)" |
|||
[matAutocomplete]="entityAliasAutocomplete"> |
|||
<button *ngIf="selectEntityAliasFormGroup.get('entityAlias').value && !disabled" |
|||
type="button" |
|||
matSuffix mat-button mat-icon-button aria-label="Clear" |
|||
(click)="clear()"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
<mat-autocomplete class="tb-autocomplete" |
|||
#entityAliasAutocomplete="matAutocomplete" |
|||
[displayWith]="displayEntityAliasFn"> |
|||
<mat-option *ngFor="let entityAlias of filteredEntityAliases | async" [value]="entityAlias"> |
|||
<span [innerHTML]="entityAlias.alias | highlight:searchText"></span> |
|||
</mat-option> |
|||
<mat-option *ngIf="!(filteredEntityAliases | async)?.length" [value]="null" class="tb-not-found"> |
|||
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> |
|||
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty"> |
|||
<span translate>entity.no-aliases-found</span> |
|||
</div> |
|||
<ng-template #searchNotEmpty> |
|||
<span> |
|||
{{ translate.get('entity.no-alias-matching', |
|||
{alias: truncate.transform(searchText, true, 6, '...')}) | async }} |
|||
</span> |
|||
</ng-template> |
|||
<span> |
|||
<a translate (click)="createEntityAlias($event, searchText)">entity.create-new-alias</a> |
|||
</span> |
|||
</div> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-error *ngIf="!modelValue && required"> |
|||
{{ 'entity.alias-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
@ -0,0 +1,23 @@ |
|||
///
|
|||
/// 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 { EntityType } from '@shared/models/entity-type.models'; |
|||
import { Observable } from 'rxjs'; |
|||
import { EntityAlias } from '@shared/models/alias.models'; |
|||
|
|||
export interface EntityAliasSelectCallbacks { |
|||
createEntityAlias: (alias: string, allowedEntityTypes: Array<EntityType>) => Observable<EntityAlias>; |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* 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 { |
|||
|
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-form-field-infix { |
|||
border-top: none; |
|||
} |
|||
} |
|||
@ -0,0 +1,258 @@ |
|||
///
|
|||
/// 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 { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
FormGroupDirective, |
|||
NG_VALUE_ACCESSOR, NgForm |
|||
} from '@angular/forms'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { map, mergeMap, startWith, tap, share, 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} 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 {coerceBooleanProperty} from '@angular/cdk/coercion'; |
|||
import { EntityAlias } from '@shared/models/alias.models'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
|||
import { MatAutocomplete, MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete'; |
|||
import { EntityAliasSelectCallbacks } from './entity-alias-select.component.models'; |
|||
import {ENTER} from '@angular/cdk/keycodes'; |
|||
import { ErrorStateMatcher } from '@angular/material/core'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-alias-select', |
|||
templateUrl: './entity-alias-select.component.html', |
|||
styleUrls: ['./entity-alias-select.component.scss'], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => EntityAliasSelectComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: ErrorStateMatcher, |
|||
useExisting: EntityAliasSelectComponent |
|||
}] |
|||
}) |
|||
export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { |
|||
|
|||
selectEntityAliasFormGroup: FormGroup; |
|||
|
|||
modelValue: string | null; |
|||
|
|||
@Input() |
|||
aliasController: IAliasController; |
|||
|
|||
@Input() |
|||
allowedEntityTypes: Array<EntityType>; |
|||
|
|||
@Input() |
|||
callbacks: EntityAliasSelectCallbacks; |
|||
|
|||
@ViewChild('entityAliasAutocomplete', {static: false}) entityAliasAutocomplete: MatAutocomplete; |
|||
@ViewChild('autocomplete', {static: false, read: MatAutocompleteTrigger}) autoCompleteTrigger: MatAutocompleteTrigger; |
|||
|
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@ViewChild('entityAliasInput', {static: true}) entityAliasInput: ElementRef; |
|||
|
|||
entityAliasList: Array<EntityAlias> = []; |
|||
|
|||
filteredEntityAliases: Observable<Array<EntityAlias>>; |
|||
|
|||
private searchText = ''; |
|||
|
|||
private dirty = false; |
|||
|
|||
private creatingEntityAlias = false; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
private entityService: EntityService, |
|||
public translate: TranslateService, |
|||
public truncate: TruncatePipe, |
|||
private fb: FormBuilder) { |
|||
this.selectEntityAliasFormGroup = this.fb.group({ |
|||
entityAlias: [null] |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
const entityAliases = this.aliasController.getEntityAliases(); |
|||
for (const aliasId of Object.keys(entityAliases)) { |
|||
if (this.allowedEntityTypes && this.allowedEntityTypes.length) { |
|||
if (!this.entityService.filterAliasByEntityTypes(entityAliases[aliasId], this.allowedEntityTypes)) { |
|||
continue; |
|||
} |
|||
} |
|||
this.entityAliasList.push(entityAliases[aliasId]); |
|||
} |
|||
|
|||
this.filteredEntityAliases = this.selectEntityAliasFormGroup.get('entityAlias').valueChanges |
|||
.pipe( |
|||
tap(value => { |
|||
let modelValue; |
|||
if (typeof value === 'string' || !value) { |
|||
modelValue = null; |
|||
} else { |
|||
modelValue = value; |
|||
} |
|||
this.updateView(modelValue); |
|||
if (value === null) { |
|||
this.clear(); |
|||
} |
|||
}), |
|||
map(value => value ? (typeof value === 'string' ? value : value.alias) : ''), |
|||
mergeMap(name => this.fetchEntityAliases(name) ), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = this.required && !this.modelValue; |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
ngAfterViewInit(): void {} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.selectEntityAliasFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.selectEntityAliasFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: string | null): void { |
|||
this.searchText = ''; |
|||
let entityAlias = null; |
|||
if (value != null) { |
|||
const entityAliases = this.aliasController.getEntityAliases(); |
|||
if (entityAliases[value]) { |
|||
entityAlias = entityAliases[value]; |
|||
} |
|||
} |
|||
if (entityAlias != null) { |
|||
this.modelValue = entityAlias.id; |
|||
this.selectEntityAliasFormGroup.get('entityAlias').patchValue(entityAlias, {emitEvent: false}); |
|||
} else { |
|||
this.modelValue = null; |
|||
this.selectEntityAliasFormGroup.get('entityAlias').patchValue('', {emitEvent: false}); |
|||
} |
|||
this.dirty = true; |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.selectEntityAliasFormGroup.get('entityAlias').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
updateView(value: EntityAlias | null) { |
|||
const aliasId = value ? value.id : null; |
|||
if (this.modelValue !== aliasId) { |
|||
this.modelValue = aliasId; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
|||
displayEntityAliasFn(entityAlias?: EntityAlias): string | undefined { |
|||
return entityAlias ? entityAlias.alias : undefined; |
|||
} |
|||
|
|||
fetchEntityAliases(searchText?: string): Observable<Array<EntityAlias>> { |
|||
this.searchText = searchText; |
|||
let result = this.entityAliasList; |
|||
if (searchText && searchText.length) { |
|||
result = this.entityAliasList.filter((entityAlias) => entityAlias.alias.toLowerCase().includes(searchText.toLowerCase())); |
|||
} |
|||
return of(result); |
|||
} |
|||
|
|||
clear(value: string = '') { |
|||
this.entityAliasInput.nativeElement.value = value; |
|||
this.selectEntityAliasFormGroup.get('entityAlias').patchValue(value, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.entityAliasInput.nativeElement.blur(); |
|||
this.entityAliasInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
textIsNotEmpty(text: string): boolean { |
|||
return (text && text != null && text.length > 0) ? true : false; |
|||
} |
|||
|
|||
entityAliasEnter($event: KeyboardEvent) { |
|||
if ($event.keyCode === ENTER) { |
|||
$event.preventDefault(); |
|||
if (!this.modelValue) { |
|||
this.createEntityAlias($event, this.searchText); |
|||
} |
|||
} |
|||
} |
|||
|
|||
createEntityAlias($event: Event, alias: string) { |
|||
$event.preventDefault(); |
|||
this.creatingEntityAlias = true; |
|||
if (this.callbacks && this.callbacks.createEntityAlias) { |
|||
this.callbacks.createEntityAlias(alias, this.allowedEntityTypes).subscribe((newAlias) => { |
|||
if (!newAlias) { |
|||
setTimeout(() => { |
|||
this.entityAliasInput.nativeElement.blur(); |
|||
this.entityAliasInput.nativeElement.focus(); |
|||
}, 0); |
|||
} else { |
|||
this.entityAliasList.push(newAlias); |
|||
this.modelValue = newAlias.id; |
|||
this.selectEntityAliasFormGroup.get('entityAlias').patchValue(newAlias, {emitEvent: true}); |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -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.
|
|||
///
|
|||
|
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { DataKey } from '@shared/models/widget.models'; |
|||
|
|||
export interface DataKeysCallbacks { |
|||
generateDataKey: (chip: any, type: DataKeyType) => DataKey; |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
///
|
|||
/// 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 { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models'; |
|||
import { DataKeysCallbacks } from './data-keys.component.models'; |
|||
|
|||
export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & DataKeysCallbacks; |
|||
@ -0,0 +1,42 @@ |
|||
///
|
|||
/// 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 {Pipe, PipeTransform} from '@angular/core'; |
|||
import { isString } from '@core/utils'; |
|||
|
|||
@Pipe({ name: 'truncate' }) |
|||
export class TruncatePipe implements PipeTransform { |
|||
transform(text: string, wordwise: boolean, max: any, tail: string): string { |
|||
if (!text) { return ''; } |
|||
if (isString(max)) { |
|||
max = parseInt(max, 10); |
|||
} |
|||
if (!max) { return text; } |
|||
if (text.length <= max) { return text; } |
|||
|
|||
text = text.substr(0, max); |
|||
if (wordwise) { |
|||
let lastspace = text.lastIndexOf(' '); |
|||
if (lastspace !== -1) { |
|||
if (text.charAt(lastspace - 1) === '.' || text.charAt(lastspace - 1) === ',') { |
|||
lastspace = lastspace - 1; |
|||
} |
|||
text = text.substr(0, lastspace); |
|||
} |
|||
} |
|||
return text + (tail || ' …'); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue