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