18 changed files with 1112 additions and 43 deletions
@ -0,0 +1,39 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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" style="height: 40px;" class="tb-json-content-toolbar"> |
||||
|
<label class="tb-title no-padding">{{ label }}</label> |
||||
|
<span fxFlex></span> |
||||
|
<button type="button" |
||||
|
mat-button *ngIf="!readonly" class="tidy" (click)="beautifyJson()"> |
||||
|
{{'js-func.tidy' | translate }} |
||||
|
</button> |
||||
|
<button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen" |
||||
|
class="tb-mat-32" |
||||
|
matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<div id="tb-json-panel" tb-toast toastTarget="jsonContentEditor" |
||||
|
class="tb-json-content-panel" fxLayout="column"> |
||||
|
<div #jsonEditor id="tb-json-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div> |
||||
|
</div> |
||||
|
</div> |
||||
@ -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. |
||||
|
*/ |
||||
|
|
||||
|
:host { |
||||
|
position: relative; |
||||
|
|
||||
|
.fill-height { |
||||
|
height: 100%; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-json-content-toolbar { |
||||
|
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { |
||||
|
align-items: center; |
||||
|
vertical-align: middle; |
||||
|
min-width: 32px; |
||||
|
min-height: 15px; |
||||
|
padding: 4px; |
||||
|
margin: 0; |
||||
|
font-size: .8rem; |
||||
|
line-height: 15px; |
||||
|
color: #7b7b7b; |
||||
|
background: rgba(220, 220, 220, .35); |
||||
|
&:not(:last-child) { |
||||
|
margin-right: 4px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.tb-json-content-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,275 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
ElementRef, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnInit, |
||||
|
ViewChild, |
||||
|
SimpleChanges, |
||||
|
OnDestroy |
||||
|
} from '@angular/core'; |
||||
|
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; |
||||
|
import * as ace from 'ace-builds'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { ContentType, contentTypesMap } from '@shared/models/constants'; |
||||
|
import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-json-content', |
||||
|
templateUrl: './json-content.component.html', |
||||
|
styleUrls: ['./json-content.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => JsonContentComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => JsonContentComponent), |
||||
|
multi: true, |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class JsonContentComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy { |
||||
|
|
||||
|
@ViewChild('jsonEditor', {static: true}) |
||||
|
jsonEditorElmRef: ElementRef; |
||||
|
|
||||
|
private jsonEditor: ace.Ace.Editor; |
||||
|
private editorsResizeCaf: CancelAnimationFrame; |
||||
|
private editorResizeListener: any; |
||||
|
|
||||
|
@Input() label: string; |
||||
|
|
||||
|
@Input() contentType: ContentType; |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() fillHeight: boolean; |
||||
|
|
||||
|
@Input() editorStyle: {[klass: string]: any}; |
||||
|
|
||||
|
private readonlyValue: boolean; |
||||
|
get readonly(): boolean { |
||||
|
return this.readonlyValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set readonly(value: boolean) { |
||||
|
this.readonlyValue = coerceBooleanProperty(value); |
||||
|
} |
||||
|
|
||||
|
private validateContentValue: boolean; |
||||
|
get validateContent(): boolean { |
||||
|
return this.validateContentValue; |
||||
|
} |
||||
|
@Input() |
||||
|
set validateContent(value: boolean) { |
||||
|
this.validateContentValue = coerceBooleanProperty(value); |
||||
|
} |
||||
|
|
||||
|
fullscreen = false; |
||||
|
|
||||
|
contentBody: string; |
||||
|
|
||||
|
contentValid: boolean; |
||||
|
|
||||
|
validationError: string; |
||||
|
|
||||
|
errorShowed = false; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
constructor(public elementRef: ElementRef, |
||||
|
protected store: Store<AppState>, |
||||
|
private raf: RafService) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
const editorElement = this.jsonEditorElmRef.nativeElement; |
||||
|
let mode = 'text'; |
||||
|
if (this.contentType) { |
||||
|
mode = contentTypesMap.get(this.contentType).code; |
||||
|
} |
||||
|
let editorOptions: Partial<ace.Ace.EditorOptions> = { |
||||
|
mode: `ace/mode/${mode}`, |
||||
|
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(true); |
||||
|
this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1); |
||||
|
this.jsonEditor.on('change', () => { |
||||
|
this.cleanupJsonErrors(); |
||||
|
this.updateView(); |
||||
|
}); |
||||
|
this.editorResizeListener = this.onAceEditorResize.bind(this); |
||||
|
// @ts-ignore
|
||||
|
addResizeListener(editorElement, this.editorResizeListener); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
if (this.editorResizeListener) { |
||||
|
const editorElement = this.jsonEditorElmRef.nativeElement; |
||||
|
// @ts-ignore
|
||||
|
removeResizeListener(editorElement, this.editorResizeListener); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onAceEditorResize() { |
||||
|
if (this.editorsResizeCaf) { |
||||
|
this.editorsResizeCaf(); |
||||
|
this.editorsResizeCaf = null; |
||||
|
} |
||||
|
this.editorsResizeCaf = this.raf.raf(() => { |
||||
|
this.jsonEditor.resize(); |
||||
|
this.jsonEditor.renderer.updateFull(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange && change.currentValue !== change.previousValue) { |
||||
|
if (propName === 'contentType') { |
||||
|
if (this.jsonEditor) { |
||||
|
let mode = 'text'; |
||||
|
if (this.contentType) { |
||||
|
mode = contentTypesMap.get(this.contentType).code; |
||||
|
} |
||||
|
this.jsonEditor.session.setMode(`ace/mode/${mode}`); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
|
||||
|
public validate(c: FormControl) { |
||||
|
return (this.contentValid) ? null : { |
||||
|
contentBody: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
validateOnSubmit(): void { |
||||
|
if (!this.readonly) { |
||||
|
this.cleanupJsonErrors(); |
||||
|
this.contentValid = true; |
||||
|
this.propagateChange(this.contentBody); |
||||
|
this.contentValid = this.doValidate(); |
||||
|
this.propagateChange(this.contentBody); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private doValidate(): boolean { |
||||
|
try { |
||||
|
if (this.validateContent && this.contentType === ContentType.JSON) { |
||||
|
JSON.parse(this.contentBody); |
||||
|
} |
||||
|
return true; |
||||
|
} catch (ex) { |
||||
|
let errorInfo = 'Error:'; |
||||
|
if (ex.name) { |
||||
|
errorInfo += ' ' + ex.name + ':'; |
||||
|
} |
||||
|
if (ex.message) { |
||||
|
errorInfo += ' ' + ex.message; |
||||
|
} |
||||
|
this.store.dispatch(new ActionNotificationShow( |
||||
|
{ |
||||
|
message: errorInfo, |
||||
|
type: 'error', |
||||
|
target: 'jsonContentEditor', |
||||
|
verticalPosition: 'bottom', |
||||
|
horizontalPosition: 'left' |
||||
|
})); |
||||
|
this.errorShowed = true; |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cleanupJsonErrors(): void { |
||||
|
if (this.errorShowed) { |
||||
|
this.store.dispatch(new ActionNotificationHide( |
||||
|
{ |
||||
|
target: 'jsonContentEditor' |
||||
|
})); |
||||
|
this.errorShowed = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: string): void { |
||||
|
this.contentBody = value; |
||||
|
this.contentValid = true; |
||||
|
if (this.jsonEditor) { |
||||
|
this.jsonEditor.setValue(this.contentBody ? this.contentBody : '', -1); |
||||
|
// this.jsonEditor.
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateView() { |
||||
|
const editorValue = this.jsonEditor.getValue(); |
||||
|
if (this.contentBody !== editorValue) { |
||||
|
this.contentBody = editorValue; |
||||
|
this.contentValid = true; |
||||
|
this.propagateChange(this.contentBody); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
beautifyJson() { |
||||
|
const res = js_beautify(this.contentBody, {indent_size: 4, wrap_line_length: 60}); |
||||
|
this.jsonEditor.setValue(res ? res : '', -1); |
||||
|
this.updateView(); |
||||
|
} |
||||
|
|
||||
|
onFullscreen() { |
||||
|
if (this.jsonEditor) { |
||||
|
setTimeout(() => { |
||||
|
this.jsonEditor.resize(); |
||||
|
}, 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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. |
||||
|
|
||||
|
--> |
||||
|
|
||||
|
<section fxLayout="column" class="tb-kv-map" [formGroup]="kvListFormGroup"> |
||||
|
<label translate class="tb-title no-padding">{{ titleText }}</label> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" style="max-height: 40px;" |
||||
|
formArrayName="keyVals" |
||||
|
*ngFor="let keyValControl of keyValsFormArray().controls; let $index = index"> |
||||
|
<mat-form-field fxFlex floatLabel="always" hideRequiredMarker class="mat-block" |
||||
|
style="max-height: 40px;"> |
||||
|
<mat-label></mat-label> |
||||
|
<input [formControl]="keyValControl.get('key')" matInput required |
||||
|
placeholder="{{ (keyPlaceholderText ? keyPlaceholderText : 'key-val.key') | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex floatLabel="always" hideRequiredMarker class="mat-block" |
||||
|
style="max-height: 40px;"> |
||||
|
<mat-label></mat-label> |
||||
|
<input [formControl]="keyValControl.get('value')" matInput required |
||||
|
placeholder="{{ (valuePlaceholderText ? valuePlaceholderText : 'key-val.value') | translate }}"/> |
||||
|
</mat-form-field> |
||||
|
<button mat-button mat-icon-button color="primary" |
||||
|
[fxShow]="!disabled" |
||||
|
type="button" |
||||
|
(click)="removeKeyVal($index)" |
||||
|
[disabled]="isLoading$ | async" |
||||
|
matTooltip="{{ 'key-val.remove-entry' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
<span [fxShow]="!keyValsFormArray().length" |
||||
|
fxLayoutAlign="center center" [ngClass]="{'disabled': disabled}" |
||||
|
class="no-data-found" translate>{{noDataText ? noDataText : 'key-val.no-data'}}</span> |
||||
|
<div style="margin-top: 8px;"> |
||||
|
<button mat-button mat-raised-button color="primary" |
||||
|
[fxShow]="!disabled" |
||||
|
[disabled]="isLoading$ | async" |
||||
|
(click)="addKeyVal()" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'key-val.add-entry' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'action.add' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</section> |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* 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-kv-map { |
||||
|
span.no-data-found { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
height: 40px; |
||||
|
text-transform: uppercase; |
||||
|
|
||||
|
&.disabled { |
||||
|
color: rgba(0, 0, 0, .38); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host ::ng-deep { |
||||
|
.mat-form-field-wrapper { |
||||
|
padding-bottom: 0; |
||||
|
} |
||||
|
.mat-form-field-infix { |
||||
|
border-top: 0; |
||||
|
} |
||||
|
.mat-form-field-underline { |
||||
|
bottom: 0; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,160 @@ |
|||||
|
///
|
||||
|
/// 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_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, Validator, |
||||
|
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-key-val-map', |
||||
|
templateUrl: './kv-map.component.html', |
||||
|
styleUrls: ['./kv-map.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => KeyValMapComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => KeyValMapComponent), |
||||
|
multi: true, |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class KeyValMapComponent extends PageComponent implements ControlValueAccessor, OnInit, Validator { |
||||
|
|
||||
|
@Input() disabled: boolean; |
||||
|
|
||||
|
@Input() titleText: string; |
||||
|
|
||||
|
@Input() keyPlaceholderText: string; |
||||
|
|
||||
|
@Input() valuePlaceholderText: string; |
||||
|
|
||||
|
@Input() noDataText: string; |
||||
|
|
||||
|
kvListFormGroup: FormGroup; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
private valueChangeSubscription: Subscription = null; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private fb: FormBuilder) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.kvListFormGroup = this.fb.group({}); |
||||
|
this.kvListFormGroup.addControl('keyVals', |
||||
|
this.fb.array([])); |
||||
|
} |
||||
|
|
||||
|
keyValsFormArray(): FormArray { |
||||
|
return this.kvListFormGroup.get('keyVals') as FormArray; |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState?(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.kvListFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.kvListFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(keyValMap: {[key: string]: string}): void { |
||||
|
if (this.valueChangeSubscription) { |
||||
|
this.valueChangeSubscription.unsubscribe(); |
||||
|
} |
||||
|
const keyValsControls: Array<AbstractControl> = []; |
||||
|
if (keyValMap) { |
||||
|
for (const property of Object.keys(keyValMap)) { |
||||
|
if (Object.prototype.hasOwnProperty.call(keyValMap, property)) { |
||||
|
keyValsControls.push(this.fb.group({ |
||||
|
key: [property, [Validators.required]], |
||||
|
value: [keyValMap[property], [Validators.required]] |
||||
|
})); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls)); |
||||
|
this.valueChangeSubscription = this.kvListFormGroup.valueChanges.subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public removeKeyVal(index: number) { |
||||
|
(this.kvListFormGroup.get('keyVals') as FormArray).removeAt(index); |
||||
|
} |
||||
|
|
||||
|
public addKeyVal() { |
||||
|
const keyValsFormArray = this.kvListFormGroup.get('keyVals') as FormArray; |
||||
|
keyValsFormArray.push(this.fb.group({ |
||||
|
key: ['', [Validators.required]], |
||||
|
value: ['', [Validators.required]] |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
public validate(c: FormControl) { |
||||
|
const kvList: {key: string; value: string}[] = this.kvListFormGroup.get('keyVals').value; |
||||
|
let valid = true; |
||||
|
for (const entry of kvList) { |
||||
|
if (!entry.key || !entry.value) { |
||||
|
valid = false; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
return (valid) ? null : { |
||||
|
keyVals: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const kvList: {key: string; value: string}[] = this.kvListFormGroup.get('keyVals').value; |
||||
|
const keyValMap: {[key: string]: string} = {}; |
||||
|
kvList.forEach((entry) => { |
||||
|
keyValMap[entry.key] = entry.value; |
||||
|
}); |
||||
|
this.propagateChange(keyValMap); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="messageTypeFormGroup" class="mat-block"> |
||||
|
<mat-label>{{ 'rulenode.message-type' | translate }}</mat-label> |
||||
|
<input matInput type="text" placeholder="{{ 'rulenode.select-message-type' | translate }}" |
||||
|
#messageTypeInput |
||||
|
formControlName="messageType" |
||||
|
(focusin)="onFocus()" |
||||
|
[required]="required" |
||||
|
[matAutocomplete]="messageTypeAutocomplete"> |
||||
|
<button *ngIf="messageTypeFormGroup.get('messageType').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" |
||||
|
#messageTypeAutocomplete="matAutocomplete" |
||||
|
[displayWith]="displayMessageTypeFn"> |
||||
|
<mat-option *ngFor="let messageType of filteredMessageTypes | async" [value]="messageType"> |
||||
|
<span [innerHTML]="displayMessageTypeFn(messageType) | highlight:searchText"></span> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
<mat-error *ngIf="messageTypeFormGroup.get('messageType').hasError('required')"> |
||||
|
{{ 'rulenode.message-type-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
@ -0,0 +1,178 @@ |
|||||
|
///
|
||||
|
/// 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, OnDestroy, OnInit, ViewChild } from '@angular/core'; |
||||
|
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { map, mergeMap, startWith, tap } from 'rxjs/operators'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@app/core/core.state'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { MessageType, messageTypeNames } from '@shared/models/rule-node.models'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-message-type-autocomplete', |
||||
|
templateUrl: './message-type-autocomplete.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => MessageTypeAutocompleteComponent), |
||||
|
multi: true |
||||
|
}] |
||||
|
}) |
||||
|
export class MessageTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { |
||||
|
|
||||
|
messageTypeFormGroup: 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('messageTypeInput', {static: true}) messageTypeInput: ElementRef; |
||||
|
|
||||
|
filteredMessageTypes: Observable<Array<MessageType | string>>; |
||||
|
|
||||
|
searchText = ''; |
||||
|
|
||||
|
private dirty = false; |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
public translate: TranslateService, |
||||
|
private fb: FormBuilder) { |
||||
|
this.messageTypeFormGroup = this.fb.group({ |
||||
|
messageType: [null] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.filteredMessageTypes = this.messageTypeFormGroup.get('messageType').valueChanges |
||||
|
.pipe( |
||||
|
tap(value => { |
||||
|
this.updateView(value); |
||||
|
}), |
||||
|
startWith<string | MessageType>(''), |
||||
|
map(value => value ? value : ''), |
||||
|
mergeMap(messageType => this.fetchMessageTypes(messageType) ) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
ngAfterViewInit(): void { |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (this.disabled) { |
||||
|
this.messageTypeFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.messageTypeFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: string | null): void { |
||||
|
this.searchText = ''; |
||||
|
this.modelValue = value; |
||||
|
let res: MessageType | string = null; |
||||
|
if (value) { |
||||
|
if (Object.values(MessageType).includes(value)) { |
||||
|
res = MessageType[value]; |
||||
|
} else { |
||||
|
res = value; |
||||
|
} |
||||
|
} |
||||
|
this.messageTypeFormGroup.get('messageType').patchValue(res, {emitEvent: false}); |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
|
||||
|
onFocus() { |
||||
|
if (this.dirty) { |
||||
|
this.messageTypeFormGroup.get('messageType').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
||||
|
this.dirty = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateView(value: MessageType | string | null) { |
||||
|
let res: string = null; |
||||
|
if (value) { |
||||
|
if (Object.values(MessageType).includes(value)) { |
||||
|
res = MessageType[value]; |
||||
|
} else { |
||||
|
res = value; |
||||
|
} |
||||
|
} |
||||
|
if (this.modelValue !== res) { |
||||
|
this.modelValue = res; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayMessageTypeFn(messageType?: MessageType | string): string | undefined { |
||||
|
if (messageType) { |
||||
|
if (Object.values(MessageType).includes(messageType)) { |
||||
|
return messageTypeNames.get(MessageType[messageType]); |
||||
|
} else { |
||||
|
return messageType; |
||||
|
} |
||||
|
} |
||||
|
return undefined; |
||||
|
} |
||||
|
|
||||
|
fetchMessageTypes(searchText?: string): Observable<Array<MessageType | string>> { |
||||
|
this.searchText = searchText; |
||||
|
const result: Array<MessageType | string> = []; |
||||
|
messageTypeNames.forEach((value, key) => { |
||||
|
if (value.toUpperCase().includes(searchText.toUpperCase())) { |
||||
|
result.push(key); |
||||
|
} |
||||
|
}); |
||||
|
if (result.length) { |
||||
|
return of(result); |
||||
|
} else { |
||||
|
return of([searchText]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clear() { |
||||
|
this.messageTypeFormGroup.get('messageType').patchValue(null, {emitEvent: true}); |
||||
|
setTimeout(() => { |
||||
|
this.messageTypeInput.nativeElement.blur(); |
||||
|
this.messageTypeInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue