17 changed files with 1099 additions and 30 deletions
@ -0,0 +1,68 @@ |
|||
<!-- |
|||
|
|||
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 #relationForm="ngForm" [formGroup]="relationFormGroup" (ngSubmit)="save()" style="min-width: 600px;"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2>{{ (isAdd ? 'relation.add' : 'relation.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 style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<tb-relation-type-autocomplete |
|||
formControlName="type" |
|||
required="true"> |
|||
</tb-relation-type-autocomplete> |
|||
<small>{{(direction === entitySearchDirection.FROM ? |
|||
'relation.to-entity' : 'relation.from-entity') | translate}}</small> |
|||
<tb-entity-list-select |
|||
formControlName="targetEntityIds" |
|||
required="true"> |
|||
</tb-entity-list-select> |
|||
<tb-json-object-edit |
|||
formControlName="additionalInfo" |
|||
label="{{ 'relation.additional-info' | translate }}"> |
|||
</tb-json-object-edit> |
|||
<div class="tb-error-messages" *ngIf="submitted && |
|||
relationFormGroup.get('additionalInfo').invalid" role="alert"> |
|||
<div translate class="tb-error-message">relation.invalid-additional-info</div> |
|||
</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)"> |
|||
{{ (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,18 @@ |
|||
/** |
|||
* 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 { |
|||
|
|||
} |
|||
@ -0,0 +1,134 @@ |
|||
///
|
|||
/// 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 { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; |
|||
import { |
|||
CONTAINS_TYPE, |
|||
EntityRelation, |
|||
EntitySearchDirection, |
|||
RelationTypeGroup |
|||
} from '@shared/models/relation.models'; |
|||
import { EntityRelationService } from '@core/http/entity-relation.service'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { Observable, forkJoin } from 'rxjs'; |
|||
|
|||
export interface RelationDialogData { |
|||
isAdd: boolean; |
|||
direction: EntitySearchDirection; |
|||
relation: EntityRelation; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-relation-dialog', |
|||
templateUrl: './relation-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: RelationDialogComponent}], |
|||
styleUrls: ['./relation-dialog.component.scss'] |
|||
}) |
|||
export class RelationDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher { |
|||
|
|||
relationFormGroup: FormGroup; |
|||
|
|||
isAdd: boolean; |
|||
direction: EntitySearchDirection; |
|||
entitySearchDirection = EntitySearchDirection; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
@Inject(MAT_DIALOG_DATA) public data: RelationDialogData, |
|||
private entityRelationService: EntityRelationService, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<RelationDialogComponent, boolean>, |
|||
public fb: FormBuilder) { |
|||
super(store); |
|||
this.isAdd = data.isAdd; |
|||
this.direction = data.direction; |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.relationFormGroup = this.fb.group({ |
|||
type: [this.isAdd ? CONTAINS_TYPE : this.data.relation.type, [Validators.required]], |
|||
targetEntityIds: [this.isAdd ? null : |
|||
[this.direction === EntitySearchDirection.FROM ? this.data.relation.to : this.data.relation.from], |
|||
[Validators.required]], |
|||
additionalInfo: [this.data.relation.additionalInfo] |
|||
}); |
|||
if (!this.isAdd) { |
|||
this.relationFormGroup.get('type').disable(); |
|||
this.relationFormGroup.get('targetEntityIds').disable(); |
|||
} |
|||
this.relationFormGroup.valueChanges.subscribe( |
|||
() => { |
|||
this.submitted = false; |
|||
} |
|||
); |
|||
} |
|||
|
|||
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(false); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
if (this.relationFormGroup.valid) { |
|||
const additionalInfo = this.relationFormGroup.get('additionalInfo').value; |
|||
if (this.isAdd) { |
|||
const tasks: Observable<EntityRelation>[] = []; |
|||
const type: string = this.relationFormGroup.get('type').value; |
|||
const entityIds: Array<EntityId> = this.relationFormGroup.get('targetEntityIds').value; |
|||
entityIds.forEach(entityId => { |
|||
const relation = { |
|||
type, |
|||
additionalInfo, |
|||
typeGroup: RelationTypeGroup.COMMON |
|||
} as EntityRelation; |
|||
if (this.direction === EntitySearchDirection.FROM) { |
|||
relation.from = this.data.relation.from; |
|||
relation.to = entityId; |
|||
} else { |
|||
relation.from = entityId; |
|||
relation.to = this.data.relation.to; |
|||
} |
|||
tasks.push(this.entityRelationService.saveRelation(relation)); |
|||
}); |
|||
forkJoin(tasks).subscribe( |
|||
() => { |
|||
this.dialogRef.close(true); |
|||
} |
|||
); |
|||
} else { |
|||
const relation: EntityRelation = {...this.data.relation}; |
|||
relation.additionalInfo = additionalInfo; |
|||
this.entityRelationService.saveRelation(relation).subscribe( |
|||
() => { |
|||
this.dialogRef.close(true); |
|||
} |
|||
); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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. |
|||
|
|||
--> |
|||
<div fxLayout="row" class="tb-entity-list-select" [formGroup]="entityListSelectFormGroup"> |
|||
<tb-entity-type-select |
|||
style="min-width: 100px; padding-right: 8px;" |
|||
*ngIf="displayEntityTypeSelect" |
|||
[showLabel]="true" |
|||
[required]="required" |
|||
[useAliasEntityTypes]="useAliasEntityTypes" |
|||
[allowedEntityTypes]="allowedEntityTypes" |
|||
formControlName="entityType"> |
|||
</tb-entity-type-select> |
|||
<tb-entity-list |
|||
[ngClass]="{'tb-not-empty': this.modelValue.ids?.length > 0}" |
|||
fxFlex |
|||
*ngIf="modelValue.entityType" |
|||
[required]="required" |
|||
[entityType]="modelValue.entityType" |
|||
formControlName="entityIds"> |
|||
</tb-entity-list> |
|||
</div> |
|||
@ -0,0 +1,32 @@ |
|||
/** |
|||
* 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 { |
|||
tb-entity-list { |
|||
&.tb-not-empty { |
|||
.mat-form-field-flex { |
|||
padding-top: 0; |
|||
} |
|||
} |
|||
.mat-form-field-flex { |
|||
.mat-form-field-infix { |
|||
border-top: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,170 @@ |
|||
///
|
|||
/// 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, forwardRef, Input, OnInit} from '@angular/core'; |
|||
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms'; |
|||
import {Store} from '@ngrx/store'; |
|||
import {AppState} from '@core/core.state'; |
|||
import {TranslateService} from '@ngx-translate/core'; |
|||
import {AliasEntityType, EntityType, entityTypeTranslations} from '@shared/models/entity-type.models'; |
|||
import {EntityService} from '@core/http/entity.service'; |
|||
import {EntityId} from '@shared/models/id/entity-id'; |
|||
import {coerceBooleanProperty} from '@angular/cdk/coercion'; |
|||
|
|||
interface EntityListSelectModel { |
|||
entityType: EntityType | AliasEntityType; |
|||
ids: Array<string>; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-entity-list-select', |
|||
templateUrl: './entity-list-select.component.html', |
|||
styleUrls: ['./entity-list-select.component.scss'], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => EntityListSelectComponent), |
|||
multi: true |
|||
}] |
|||
}) |
|||
|
|||
export class EntityListSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit { |
|||
|
|||
entityListSelectFormGroup: FormGroup; |
|||
|
|||
modelValue: EntityListSelectModel = {entityType: null, ids: []}; |
|||
|
|||
@Input() |
|||
allowedEntityTypes: Array<EntityType | AliasEntityType>; |
|||
|
|||
@Input() |
|||
useAliasEntityTypes: boolean; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
displayEntityTypeSelect: boolean; |
|||
|
|||
private defaultEntityType: EntityType | AliasEntityType = null; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private entityService: EntityService, |
|||
public translate: TranslateService, |
|||
private fb: FormBuilder) { |
|||
|
|||
const entityTypes = this.entityService.prepareAllowedEntityTypesList(this.allowedEntityTypes, |
|||
this.useAliasEntityTypes); |
|||
if (entityTypes.length === 1) { |
|||
this.displayEntityTypeSelect = false; |
|||
this.defaultEntityType = entityTypes[0]; |
|||
} else { |
|||
this.displayEntityTypeSelect = true; |
|||
} |
|||
|
|||
this.entityListSelectFormGroup = this.fb.group({ |
|||
entityType: [this.defaultEntityType], |
|||
entityIds: [[]] |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.entityListSelectFormGroup.get('entityType').valueChanges.subscribe( |
|||
(value) => { |
|||
this.updateView(value, this.modelValue.ids); |
|||
} |
|||
); |
|||
this.entityListSelectFormGroup.get('entityIds').valueChanges.subscribe( |
|||
(values) => { |
|||
this.updateView(this.modelValue.entityType, values); |
|||
} |
|||
); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.entityListSelectFormGroup.disable(); |
|||
} else { |
|||
this.entityListSelectFormGroup.enable(); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: Array<EntityId> | null): void { |
|||
if (value != null && value.length > 0) { |
|||
const id = value[0]; |
|||
this.modelValue = { |
|||
entityType: id.entityType, |
|||
ids: value.map(val => val.id) |
|||
}; |
|||
} else { |
|||
this.modelValue = { |
|||
entityType: this.defaultEntityType, |
|||
ids: [] |
|||
}; |
|||
} |
|||
this.entityListSelectFormGroup.get('entityType').patchValue(this.modelValue.entityType, {emitEvent: true}); |
|||
this.entityListSelectFormGroup.get('entityIds').patchValue([...this.modelValue.ids], {emitEvent: true}); |
|||
} |
|||
|
|||
updateView(entityType: EntityType | AliasEntityType | null, entityIds: Array<string> | null) { |
|||
if (this.modelValue.entityType !== entityType || |
|||
!this.compareIds(this.modelValue.ids, entityIds)) { |
|||
this.modelValue = { |
|||
entityType, |
|||
ids: this.modelValue.entityType !== entityType || !entityIds ? [] : [...entityIds] |
|||
}; |
|||
this.propagateChange(this.toEntityIds(this.modelValue)); |
|||
} |
|||
} |
|||
|
|||
compareIds(ids1: Array<string> | null, ids2: Array<string> | null): boolean { |
|||
if (ids1 !== null && ids2 !== null) { |
|||
return JSON.stringify(ids1) === JSON.stringify(ids2); |
|||
} else { |
|||
return ids1 === ids2; |
|||
} |
|||
} |
|||
|
|||
toEntityIds(modelValue: EntityListSelectModel): Array<EntityId> { |
|||
if (modelValue !== null && modelValue.entityType && modelValue.ids && modelValue.ids.length > 0) { |
|||
const entityType = modelValue.entityType; |
|||
return modelValue.ids.map(id => ({entityType, id})); |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<!-- |
|||
|
|||
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 style="background: #fff;" [ngClass]="{'fill-height': fillHeight}" |
|||
tb-fullscreen [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column"> |
|||
<div fxLayout="row" fxLayoutAlign="start center"> |
|||
<label class="tb-title no-padding" |
|||
ng-class="{'tb-required': required, |
|||
'tb-readonly': readonly, |
|||
'tb-error': !objectValid}">{{ label }}</label> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-icon-button (click)="fullscreen = !fullscreen" |
|||
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
|||
</button> |
|||
</div> |
|||
<div fxFlex="0%" id="tb-json-panel" class="tb-json-object-panel" fxLayout="column"> |
|||
<div fxFlex #jsonEditor id="tb-json-input" [ngClass]="{'fill-height': fillHeight}"></div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,55 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
|
|||
/** |
|||
* 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 { |
|||
position: relative; |
|||
|
|||
.fill-height { |
|||
height: 100%; |
|||
} |
|||
|
|||
.tb-json-object-panel { |
|||
height: 100%; |
|||
margin-left: 15px; |
|||
border: 1px solid #c0c0c0; |
|||
|
|||
#tb-json-input { |
|||
width: 100%; |
|||
min-width: 200px; |
|||
height: 100%; |
|||
|
|||
&:not(.fill-height) { |
|||
min-height: 200px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,180 @@ |
|||
///
|
|||
/// 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 { |
|||
Attribute, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
Input, |
|||
OnInit, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, Validator, NG_VALIDATORS } from '@angular/forms'; |
|||
import * as ace from 'ace-builds'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-json-object-edit', |
|||
templateUrl: './json-object-edit.component.html', |
|||
styleUrls: ['./json-object-edit.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => JsonObjectEditComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => JsonObjectEditComponent), |
|||
multi: true, |
|||
} |
|||
] |
|||
}) |
|||
export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Validator { |
|||
|
|||
@ViewChild('jsonEditor', {static: true}) |
|||
jsonEditorElmRef: ElementRef; |
|||
|
|||
private jsonEditor: ace.Ace.Editor; |
|||
|
|||
@Input() label: string; |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() fillHeight: boolean; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
private readonlyValue: boolean; |
|||
get readonly(): boolean { |
|||
return this.readonlyValue; |
|||
} |
|||
@Input() |
|||
set readonly(value: boolean) { |
|||
this.readonlyValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
fullscreen = false; |
|||
|
|||
modelValue: any; |
|||
|
|||
contentValue: string; |
|||
|
|||
objectValid: boolean; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor() { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
const editorElement = this.jsonEditorElmRef.nativeElement; |
|||
let editorOptions: Partial<ace.Ace.EditorOptions> = { |
|||
mode: 'ace/mode/json', |
|||
theme: 'ace/theme/github', |
|||
showGutter: true, |
|||
showPrintMargin: false, |
|||
readOnly: this.readonly |
|||
}; |
|||
|
|||
const advancedOptions = { |
|||
enableSnippets: true, |
|||
enableBasicAutocompletion: true, |
|||
enableLiveAutocompletion: true |
|||
}; |
|||
|
|||
editorOptions = {...editorOptions, ...advancedOptions}; |
|||
this.jsonEditor = ace.edit(editorElement, editorOptions); |
|||
this.jsonEditor.session.setUseWrapMode(false); |
|||
this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1); |
|||
this.jsonEditor.on('change', () => { |
|||
this.updateView(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
} |
|||
|
|||
public validate(c: FormControl) { |
|||
return (this.objectValid) ? null : { |
|||
jsonParseError: { |
|||
valid: false, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
writeValue(value: any): void { |
|||
this.modelValue = value; |
|||
this.contentValue = ''; |
|||
this.objectValid = false; |
|||
try { |
|||
if (this.modelValue) { |
|||
this.contentValue = JSON.stringify(this.modelValue, undefined, 2); |
|||
this.objectValid = true; |
|||
} else { |
|||
this.objectValid = !this.required; |
|||
} |
|||
} catch (e) { |
|||
//
|
|||
} |
|||
if (this.jsonEditor) { |
|||
this.jsonEditor.setValue(this.contentValue ? this.contentValue : '', -1); |
|||
} |
|||
} |
|||
|
|||
updateView() { |
|||
const editorValue = this.jsonEditor.getValue(); |
|||
if (this.contentValue !== editorValue) { |
|||
this.contentValue = editorValue; |
|||
let data = null; |
|||
this.objectValid = false; |
|||
if (this.contentValue && this.contentValue.length > 0) { |
|||
try { |
|||
data = JSON.parse(this.contentValue); |
|||
this.objectValid = true; |
|||
} catch (ex) {} |
|||
} else { |
|||
this.objectValid = !this.required; |
|||
} |
|||
this.propagateChange(data); |
|||
} |
|||
} |
|||
|
|||
onFullscreen() { |
|||
if (this.jsonEditor) { |
|||
setTimeout(() => { |
|||
this.jsonEditor.resize(); |
|||
}, 0); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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. |
|||
|
|||
--> |
|||
<mat-form-field [formGroup]="relationTypeFormGroup" class="mat-block"> |
|||
<input matInput type="text" placeholder="{{ required ? ('relation.relation-type' | translate) : ( !modelValue ? ('relation.any-relation-type' | translate) : ' ') }}" |
|||
#relationTypeInput |
|||
formControlName="relationType" |
|||
(focusin)="onFocus()" |
|||
[required]="required" |
|||
[matAutocomplete]="relationTypeAutocomplete"> |
|||
<button *ngIf="relationTypeFormGroup.get('relationType').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" |
|||
#relationTypeAutocomplete="matAutocomplete" |
|||
[displayWith]="displayRelationTypeFn"> |
|||
<mat-option *ngFor="let relationType of filteredRelationTypes | async" [value]="relationType"> |
|||
<span [innerHTML]="relationType | highlight:searchText"></span> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-error *ngIf="relationTypeFormGroup.get('relationType').hasError('required')"> |
|||
{{ 'relation.relation-type-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
@ -0,0 +1,168 @@ |
|||
///
|
|||
/// 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, ViewChild, OnDestroy} from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import {Observable, of, throwError, Subscription} from 'rxjs'; |
|||
import {PageLink} from '@shared/models/page/page-link'; |
|||
import {Direction} from '@shared/models/page/sort-order'; |
|||
import {filter, map, mergeMap, publishReplay, refCount, startWith, tap, publish} from 'rxjs/operators'; |
|||
import {PageData, emptyPageData} from '@shared/models/page/page-data'; |
|||
import {DashboardInfo} from '@app/shared/models/dashboard.models'; |
|||
import {DashboardId} from '@app/shared/models/id/dashboard-id'; |
|||
import {DashboardService} from '@core/http/dashboard.service'; |
|||
import {Store} from '@ngrx/store'; |
|||
import {AppState} from '@app/core/core.state'; |
|||
import {getCurrentAuthUser} from '@app/core/auth/auth.selectors'; |
|||
import {Authority} from '@shared/models/authority.enum'; |
|||
import {TranslateService} from '@ngx-translate/core'; |
|||
import {DeviceService} from '@core/http/device.service'; |
|||
import {EntitySubtype, EntityType} from '@app/shared/models/entity-type.models'; |
|||
import {BroadcastService} from '@app/core/services/broadcast.service'; |
|||
import {coerceBooleanProperty} from '@angular/cdk/coercion'; |
|||
import {AssetService} from '@core/http/asset.service'; |
|||
import {EntityViewService} from '@core/http/entity-view.service'; |
|||
import { RelationTypes } from '@app/shared/models/relation.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-relation-type-autocomplete', |
|||
templateUrl: './relation-type-autocomplete.component.html', |
|||
styleUrls: [], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => RelationTypeAutocompleteComponent), |
|||
multi: true |
|||
}] |
|||
}) |
|||
export class RelationTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { |
|||
|
|||
relationTypeFormGroup: FormGroup; |
|||
|
|||
modelValue: string | null; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@ViewChild('relationTypeInput', {static: true}) relationTypeInput: ElementRef; |
|||
|
|||
filteredRelationTypes: Observable<Array<string>>; |
|||
|
|||
private searchText = ''; |
|||
|
|||
private dirty = false; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private broadcast: BroadcastService, |
|||
public translate: TranslateService, |
|||
private fb: FormBuilder) { |
|||
this.relationTypeFormGroup = this.fb.group({ |
|||
relationType: [null, this.required ? [Validators.required] : []] |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
|
|||
this.filteredRelationTypes = this.relationTypeFormGroup.get('relationType').valueChanges |
|||
.pipe( |
|||
tap(value => { |
|||
this.updateView(value); |
|||
}), |
|||
// startWith<string | EntitySubtype>(''),
|
|||
map(value => value ? value : ''), |
|||
mergeMap(type => this.fetchRelationTypes(type) ) |
|||
); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.relationTypeFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.relationTypeFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: string | null): void { |
|||
this.searchText = ''; |
|||
this.modelValue = value; |
|||
this.relationTypeFormGroup.get('relationType').patchValue(value, {emitEvent: false}); |
|||
this.dirty = true; |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.relationTypeFormGroup.get('relationType').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
updateView(value: string | null) { |
|||
if (this.modelValue !== value) { |
|||
this.modelValue = value; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
|||
displayRelationTypeFn(relationType?: string): string | undefined { |
|||
return relationType ? relationType : undefined; |
|||
} |
|||
|
|||
fetchRelationTypes(searchText?: string, strictMatch: boolean = false): Observable<Array<string>> { |
|||
this.searchText = searchText; |
|||
return of(RelationTypes).pipe( |
|||
map(relationTypes => relationTypes.filter( relationType => { |
|||
if (strictMatch) { |
|||
return searchText ? relationType === searchText : false; |
|||
} else { |
|||
return searchText ? relationType.toUpperCase().startsWith(searchText.toUpperCase()) : true; |
|||
} |
|||
})) |
|||
); |
|||
} |
|||
|
|||
clear() { |
|||
this.relationTypeFormGroup.get('relationType').patchValue(null, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.relationTypeInput.nativeElement.blur(); |
|||
this.relationTypeInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue