54 changed files with 14931 additions and 8904 deletions
File diff suppressed because it is too large
@ -0,0 +1,54 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<form #dataKeyForm="ngForm" [formGroup]="dataKeyFormGroup" (ngSubmit)="save()" style="width: 600px;"> |
|||
<mat-toolbar fxLayout="row" color="primary"> |
|||
<h2>{{ 'datakey.configuration' | 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> |
|||
<tb-data-key-config #dataKeyConfig |
|||
[dataKeySettingsSchema]="data.dataKeySettingsSchema" |
|||
[entityAliasId]="data.entityAliasId" |
|||
[callbacks]="data.callbacks" |
|||
formControlName="dataKey"> |
|||
</tb-data-key-config> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || dataKeyForm.invalid || !dataKeyForm.dirty"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
<button mat-button color="primary" |
|||
style="margin-right: 20px;" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,21 @@ |
|||
/** |
|||
* 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 { |
|||
.mat-dialog-content { |
|||
padding: 0 !important; |
|||
margin: 0 !important; |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
///
|
|||
/// 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, ViewChild } from '@angular/core'; |
|||
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
|||
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 { forkJoin, Observable } from 'rxjs'; |
|||
import { JsonObjectEditComponent } from '@app/shared/components/json-object-edit.component'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
|||
import { AttributeService } from '@core/http/attribute.service'; |
|||
import { DataKey } from '@app/shared/models/widget.models'; |
|||
import { EntityAlias } from '@shared/models/alias.models'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { DataKeysCallbacks } from './data-keys.component.models'; |
|||
import { JsFuncComponent } from '@shared/components/js-func.component'; |
|||
import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; |
|||
|
|||
export interface DataKeyConfigDialogData { |
|||
dataKey: DataKey; |
|||
dataKeySettingsSchema: any; |
|||
entityAliasId?: string; |
|||
callbacks?: DataKeysCallbacks; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-data-key-config-dialog', |
|||
templateUrl: './data-key-config-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: DataKeyConfigDialogComponent}], |
|||
styleUrls: ['./data-key-config-dialog.component.scss'] |
|||
}) |
|||
export class DataKeyConfigDialogComponent extends DialogComponent<DataKeyConfigDialogComponent, DataKey> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
@ViewChild('dataKeyConfig', {static: true}) dataKeyConfig: DataKeyConfigComponent; |
|||
|
|||
dataKeyFormGroup: FormGroup; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: DataKeyConfigDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<DataKeyConfigDialogComponent, DataKey>, |
|||
public fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.dataKeyFormGroup = this.fb.group({ |
|||
dataKey: [this.data.dataKey, [Validators.required]] |
|||
}); |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = !!(control && control.invalid && this.submitted); |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
save(): void { |
|||
this.submitted = true; |
|||
this.dataKeyConfig.validateOnSubmit(); |
|||
if (this.dataKeyFormGroup.valid) { |
|||
const dataKey: DataKey = this.dataKeyFormGroup.get('dataKey').value; |
|||
this.dialogRef.close(dataKey); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
<!-- |
|||
|
|||
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-tab-group class="tb-datakey-config" [ngClass]="{'tb-headless': !displayAdvanced}" |
|||
[formGroup]="dataKeyFormGroup" |
|||
class="tb-datakey-config"> |
|||
<mat-tab label="{{ 'datakey.settings' | translate }}"> |
|||
<div class="mat-content mat-padding" fxLayout="column"> |
|||
<mat-form-field class="mat-block" *ngIf="modelValue.type !== dataKeyTypes.function"> |
|||
<mat-label>{{ 'entity.key' | translate }}</mat-label> |
|||
<input matInput type="text" placeholder="{{ 'entity.key-name' | translate }}" |
|||
#keyInput |
|||
formControlName="name" |
|||
required |
|||
[matAutocomplete]="keyAutocomplete"> |
|||
<button *ngIf="dataKeyFormGroup.get('name').value" |
|||
type="button" |
|||
matSuffix mat-button mat-icon-button aria-label="Clear" |
|||
(click)="clearKey()"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
<mat-autocomplete |
|||
class="tb-autocomplete" |
|||
#keyAutocomplete="matAutocomplete"> |
|||
<mat-option *ngFor="let key of filteredKeys | async" [value]="key"> |
|||
<span [innerHTML]="key | highlight:keySearchText"></span> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
</mat-form-field> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>datakey.label</mat-label> |
|||
<input matInput formControlName="label" required> |
|||
</mat-form-field> |
|||
<tb-color-input fxFlex |
|||
required |
|||
label="{{'datakey.color' | translate}}" |
|||
icon="format_color_fill" |
|||
openOnInput |
|||
formControlName="color"> |
|||
</tb-color-input> |
|||
</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" *ngIf="modelValue.type !== dataKeyTypes.alarm"> |
|||
<mat-form-field fxFlex> |
|||
<mat-label translate>datakey.units</mat-label> |
|||
<input matInput formControlName="units"> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex> |
|||
<mat-label translate>datakey.decimals</mat-label> |
|||
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1"> |
|||
</mat-form-field> |
|||
</div> |
|||
<section fxLayout="column" *ngIf="modelValue.type === dataKeyTypes.function"> |
|||
<span translate>datakey.data-generation-func</span> |
|||
<br/> |
|||
<tb-js-func #funcBodyEdit |
|||
[functionArgs]="['time', 'prevValue']" |
|||
[validationArgs]="[[1, 1],[1, '1']]" |
|||
resultType="any" |
|||
formControlName="funcBody"> |
|||
</tb-js-func> |
|||
</section> |
|||
<section fxLayout="column" *ngIf="modelValue.type === dataKeyTypes.timeseries || modelValue.type === dataKeyTypes.attribute"> |
|||
<mat-checkbox formControlName="usePostProcessing"> |
|||
{{ 'datakey.use-data-post-processing-func' | translate }} |
|||
</mat-checkbox> |
|||
<tb-js-func *ngIf="dataKeyFormGroup.get('usePostProcessing').value" #postFuncBodyEdit |
|||
[functionArgs]="['time', 'value', 'prevValue', 'timePrev', 'prevOrigValue']" |
|||
[validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]" |
|||
resultType="any" |
|||
formControlName="postFuncBody"> |
|||
</tb-js-func> |
|||
<label *ngIf="dataKeyFormGroup.get('usePostProcessing').value" class="tb-title" style="margin-left: 15px;"> |
|||
time - {{ 'datakey.time-description' | translate }}<br/> |
|||
value - {{ 'datakey.value-description' | translate }}<br/> |
|||
prevValue - {{ 'datakey.prev-value-description' | translate }}<br/> |
|||
timePrev - {{ 'datakey.time-prev-description' | translate }}<br/> |
|||
prevOrigValue - {{ 'datakey.prev-orig-value-description' | translate }} |
|||
</label> |
|||
</section> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{ 'datakey.advanced' | translate }}" *ngIf="displayAdvanced"> |
|||
<div class="mat-content mat-padding" fxLayout="column"> |
|||
<div style="overflow: auto;"> |
|||
<tb-json-form |
|||
[schema]="dataKeySchema" |
|||
[form]="dataKeyForm" |
|||
formControlName="settings"> |
|||
</tb-json-form> |
|||
</div> |
|||
</div> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
@ -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 { |
|||
.tb-datakey-config { |
|||
min-width: 500px !important; |
|||
min-height: 500px !important; |
|||
height: 500px !important; |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-datakey-config { |
|||
.mat-tab-body.mat-tab-body-active { |
|||
.mat-tab-body-content > div { |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,227 @@ |
|||
///
|
|||
/// 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, OnInit, ViewChild } from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { DataKey } from '@shared/models/widget.models'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { map, mergeMap, tap } from 'rxjs/operators'; |
|||
import { alarmFields } from '@shared/models/alarm.models'; |
|||
import { JsFuncComponent } from '@shared/components/js-func.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-data-key-config', |
|||
templateUrl: './data-key-config.component.html', |
|||
styleUrls: ['./data-key-config.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => DataKeyConfigComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => DataKeyConfigComponent), |
|||
multi: true, |
|||
} |
|||
] |
|||
}) |
|||
export class DataKeyConfigComponent extends PageComponent implements OnInit, ControlValueAccessor, Validator { |
|||
|
|||
dataKeyTypes = DataKeyType; |
|||
|
|||
@Input() |
|||
entityAliasId: string; |
|||
|
|||
@Input() |
|||
callbacks: DataKeysCallbacks; |
|||
|
|||
@Input() |
|||
dataKeySettingsSchema: any; |
|||
|
|||
@ViewChild('keyInput', {static: false}) keyInput: ElementRef; |
|||
|
|||
@ViewChild('funcBodyEdit', {static: false}) funcBodyEdit: JsFuncComponent; |
|||
@ViewChild('postFuncBodyEdit', {static: false}) postFuncBodyEdit: JsFuncComponent; |
|||
|
|||
displayAdvanced = false; |
|||
|
|||
dataKeySchema: any; |
|||
dataKeyForm: any; |
|||
|
|||
private modelValue: DataKey; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
public dataKeyFormGroup: FormGroup; |
|||
|
|||
private alarmKeys: Array<DataKey>; |
|||
|
|||
filteredKeys: Observable<Array<string>>; |
|||
private latestKeySearchResult: Array<string> = null; |
|||
|
|||
keySearchText = ''; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private utils: UtilsService, |
|||
private entityService: EntityService, |
|||
private dialog: MatDialog, |
|||
private translate: TranslateService, |
|||
private fb: FormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.alarmKeys = []; |
|||
for (const name of Object.keys(alarmFields)) { |
|||
this.alarmKeys.push({ |
|||
name, |
|||
type: DataKeyType.alarm |
|||
}); |
|||
} |
|||
if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) { |
|||
this.displayAdvanced = true; |
|||
this.dataKeySchema = this.dataKeySettingsSchema.schema; |
|||
this.dataKeyForm = this.dataKeySettingsSchema.form || ['*']; |
|||
} |
|||
this.dataKeyFormGroup = this.fb.group({ |
|||
name: [null, []], |
|||
label: [null, [Validators.required]], |
|||
color: [null, [Validators.required]], |
|||
units: [null, []], |
|||
decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], |
|||
funcBody: [null, []], |
|||
usePostProcessing: [null, []], |
|||
postFuncBody: [null, []], |
|||
settings: [null, []] |
|||
}); |
|||
|
|||
this.dataKeyFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
|
|||
this.dataKeyFormGroup.get('usePostProcessing').valueChanges.subscribe((usePostProcessing: boolean) => { |
|||
const postFuncBody: string = this.dataKeyFormGroup.get('postFuncBody').value; |
|||
if (usePostProcessing && (!postFuncBody || !postFuncBody.length)) { |
|||
this.dataKeyFormGroup.get('postFuncBody').patchValue('return value;'); |
|||
} else if (!usePostProcessing && postFuncBody && postFuncBody.length) { |
|||
this.dataKeyFormGroup.get('postFuncBody').patchValue(null); |
|||
} |
|||
}); |
|||
|
|||
this.filteredKeys = this.dataKeyFormGroup.get('name').valueChanges |
|||
.pipe( |
|||
map(value => value ? value : ''), |
|||
mergeMap(name => this.fetchKeys(name) ) |
|||
); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
} |
|||
|
|||
writeValue(value: DataKey): void { |
|||
this.modelValue = value; |
|||
this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); |
|||
this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function ? [Validators.required] : []); |
|||
this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false}); |
|||
} |
|||
|
|||
private updateModel() { |
|||
this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
|
|||
clearKey() { |
|||
this.dataKeyFormGroup.get('name').patchValue(null, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.keyInput.nativeElement.blur(); |
|||
this.keyInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
private fetchKeys(searchText?: string): Observable<Array<string>> { |
|||
if (this.latestKeySearchResult === null || this.keySearchText !== searchText) { |
|||
this.keySearchText = searchText; |
|||
let fetchObservable: Observable<Array<DataKey>> = null; |
|||
if (this.modelValue.type === DataKeyType.alarm) { |
|||
const dataKeyFilter = this.createDataKeyFilter(this.keySearchText); |
|||
fetchObservable = of(this.alarmKeys.filter(dataKeyFilter)); |
|||
} else { |
|||
if (this.entityAliasId) { |
|||
const dataKeyTypes = [this.modelValue.type]; |
|||
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, this.keySearchText, dataKeyTypes); |
|||
} else { |
|||
fetchObservable = of([]); |
|||
} |
|||
} |
|||
return fetchObservable.pipe( |
|||
map((dataKeys) => dataKeys.map((dataKey) => dataKey.name)), |
|||
tap(res => this.latestKeySearchResult = res) |
|||
); |
|||
} |
|||
return of(this.latestKeySearchResult); |
|||
} |
|||
|
|||
private createDataKeyFilter(query: string): (key: DataKey) => boolean { |
|||
const lowercaseQuery = query.toLowerCase(); |
|||
return key => key.name.toLowerCase().indexOf(lowercaseQuery) === 0; |
|||
} |
|||
|
|||
public validateOnSubmit() { |
|||
if (this.modelValue.type === DataKeyType.function) { |
|||
this.funcBodyEdit.validateOnSubmit(); |
|||
} else if ((this.modelValue.type === DataKeyType.timeseries || |
|||
this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value) { |
|||
this.postFuncBodyEdit.validateOnSubmit(); |
|||
} |
|||
} |
|||
|
|||
public validate(c: FormControl) { |
|||
if (!this.dataKeyFormGroup.valid) { |
|||
return { |
|||
dataKey: { |
|||
valid: false |
|||
} |
|||
}; |
|||
} |
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,135 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-form-field appearance="standard" [formGroup]="keysListFormGroup" class="mat-block"> |
|||
<mat-chip-list #chipList formControlName="keys" tb-chip-draggable (chipDrop)="onChipDrop($event)"> |
|||
<mat-chip |
|||
*ngFor="let key of keys; let $index = index" |
|||
[selectable]="!disabled" |
|||
[removable]="!disabled" |
|||
(removed)="remove(key)"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" class="tb-attribute-chip"> |
|||
<div *ngIf="!disabled" class="tb-chip-drag-handle" style="margin-right: 5px;"> |
|||
<mat-icon class="tb-mat-20">drag_handle</mat-icon> |
|||
</div> |
|||
<div class="tb-color-preview" (click)="showColorPicker(key)" style="margin-right: 5px;"> |
|||
<div class="tb-color-result" [ngStyle]="{background: key.color}"></div> |
|||
</div> |
|||
<div fxLayout="row"> |
|||
<div class="tb-chip-label"> |
|||
<span *ngIf="datasourceType !== datasourceTypes.function && widgetType !== widgetTypes.alarm"> |
|||
<span *ngIf="key.type === dataKeyTypes.attribute" |
|||
matTooltip="{{'datakey.attributes' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon class="tb-mat-20" |
|||
svgIcon="alpha-a-circle-outline"></mat-icon> |
|||
</span> |
|||
<span *ngIf="key.type === dataKeyTypes.timeseries" |
|||
matTooltip="{{'datakey.timeseries' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon class="tb-mat-20">timeline</mat-icon> |
|||
</span> |
|||
</span> |
|||
{{key.label}} |
|||
</div> |
|||
<div class="tb-chip-separator">: </div> |
|||
<div class="tb-chip-label"> |
|||
<strong *ngIf="datasourceType !== datasourceTypes.function && key.postFuncBody; else simpleChipLabel">f({{key.name}})</strong> |
|||
<ng-template #simpleChipLabel> |
|||
<strong>{{key.name}}</strong> |
|||
</ng-template> |
|||
</div> |
|||
</div> |
|||
<button *ngIf="!disabled" (click)="editDataKey(key, $index)" mat-button mat-icon-button class="tb-mat-32"> |
|||
<mat-icon class="tb-mat-20">edit</mat-icon> |
|||
</button> |
|||
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> |
|||
</div> |
|||
</mat-chip> |
|||
<input matInput type="text" placeholder="{{ !disabled ? placeholder : '' }}" |
|||
style="max-width: 200px;" |
|||
#keyInput |
|||
formControlName="key" |
|||
matAutocompleteOrigin |
|||
#origin="matAutocompleteOrigin" |
|||
[matAutocompleteConnectedTo]="origin" |
|||
(focusin)="onFocus()" |
|||
[matAutocomplete]="keyAutocomplete" |
|||
[matChipInputFor]="chipList" |
|||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
|||
(matChipInputTokenEnd)="add($event)"> |
|||
</mat-chip-list> |
|||
<mat-autocomplete #keyAutocomplete="matAutocomplete" |
|||
class="tb-autocomplete" |
|||
[displayWith]="displayKeyFn"> |
|||
<mat-option *ngFor="let key of filteredKeys | async" [value]="key"> |
|||
<span style="white-space: nowrap;"> |
|||
<span *ngIf="datasourceType !== datasourceTypes.function && widgetType !== widgetTypes.alarm"> |
|||
<span *ngIf="key.type === dataKeyTypes.attribute" |
|||
matTooltip="{{'datakey.attributes' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon class="tb-mat-16" |
|||
svgIcon="alpha-a-circle-outline"></mat-icon> |
|||
</span> |
|||
<span *ngIf="key.type === dataKeyTypes.timeseries" |
|||
matTooltip="{{'datakey.timeseries' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon class="tb-mat-16">timeline</mat-icon> |
|||
</span> |
|||
</span> |
|||
<span [innerHTML]="key.name | highlight:searchText"></span> |
|||
</span> |
|||
</mat-option> |
|||
<mat-option *ngIf="!(filteredKeys | async)?.length" [value]="null" class="tb-not-found"> |
|||
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> |
|||
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty"> |
|||
<span translate>entity.no-keys-found</span> |
|||
</div> |
|||
<ng-template #searchNotEmpty> |
|||
<span> |
|||
{{ translate.get('entity.no-key-matching', |
|||
{key: truncate.transform(searchText, true, 6, '...')}) | async }} |
|||
</span> |
|||
<span *ngIf="datasourceType === datasourceTypes.function || widgetType === widgetTypes.alarm; else createEntityKey"> |
|||
<a translate (click)="createKey(searchText)">entity.create-new-key</a> |
|||
</span> |
|||
<ng-template #createEntityKey> |
|||
<span>{{'entity.create-new-key' | translate }} </span> |
|||
<span *ngIf="widgetType == widgetTypes.latest" |
|||
matTooltip="{{'datakey.attributes' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon (click)="createKey(searchText, dataKeyTypes.attribute)" |
|||
class="tb-mat-16" |
|||
svgIcon="alpha-a-circle-outline"></mat-icon> |
|||
</span> |
|||
<span matTooltip="{{'datakey.timeseries' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon (click)="createKey(searchText, dataKeyTypes.timeseries)" |
|||
class="tb-mat-16">timeline</mat-icon> |
|||
</span> |
|||
</ng-template> |
|||
</ng-template> |
|||
</div> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-error *ngIf="keysListFormGroup.get('keys').hasError('required')"> |
|||
{{ requiredText }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<div class="mat-caption" style="color: rgba(0,0,0,0.57);" *ngIf="maxDataKeys !== null && maxDataKeys > -1"> |
|||
{{ maxDataKeysText() }} |
|||
</div> |
|||
@ -0,0 +1,91 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
|
|||
@mixin tb-checkered-bg() { |
|||
background-color: #fff; |
|||
background-image: |
|||
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd), |
|||
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd); |
|||
background-position: 0 0, 4px 4px; |
|||
background-size: 8px 8px; |
|||
} |
|||
|
|||
:host { |
|||
.mat-chip.mat-standard-chip { |
|||
.tb-attribute-chip { |
|||
color: rgb(66, 66, 66); |
|||
font-weight: normal; |
|||
font-size: 16px; |
|||
|
|||
.tb-chip-drag-handle { |
|||
cursor: move; |
|||
|
|||
mat-icon { |
|||
pointer-events: none; |
|||
} |
|||
} |
|||
|
|||
.tb-chip-label { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.tb-chip-separator { |
|||
white-space: pre; |
|||
} |
|||
|
|||
.mat-chip-remove.mat-icon { |
|||
width: 24px; |
|||
height: 24px; |
|||
font-size: 24px; |
|||
margin-left: 0px; |
|||
color: inherit; |
|||
opacity: inherit; |
|||
} |
|||
|
|||
.tb-color-preview { |
|||
cursor: pointer; |
|||
box-sizing: border-box; |
|||
position: relative; |
|||
width: 24px; |
|||
min-width: 24px; |
|||
height: 24px; |
|||
overflow: hidden; |
|||
content: ""; |
|||
border: 2px solid #fff; |
|||
border-radius: 50%; |
|||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084); |
|||
|
|||
@include tb-checkered-bg(); |
|||
|
|||
.tb-color-result { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-form-field-flex { |
|||
padding-top: 0; |
|||
.mat-form-field-infix { |
|||
border-top: 0; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,454 @@ |
|||
///
|
|||
/// 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 { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
|||
import { |
|||
AfterViewInit, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
Input, |
|||
OnChanges, |
|||
OnInit, |
|||
SimpleChanges, SkipSelf, |
|||
ViewChild |
|||
} from '@angular/core'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, FormGroupDirective, |
|||
NG_VALUE_ACCESSOR, NgForm, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@app/core/core.state'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatAutocomplete, MatChipInputEvent, MatChipList } from '@angular/material'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; |
|||
import { IAliasController } from '@core/api/widget-api.models'; |
|||
import { DataKeysCallbacks } from './data-keys.component.models'; |
|||
import { alarmFields } from '@shared/models/alarm.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { ErrorStateMatcher } from '@angular/material/core'; |
|||
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { |
|||
ColorPickerDialogComponent, |
|||
ColorPickerDialogData |
|||
} from '@shared/components/dialog/color-picker-dialog.component'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { |
|||
DataKeyConfigDialogComponent, |
|||
DataKeyConfigDialogData |
|||
} from '@home/components/widget/data-key-config-dialog.component'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { MatChipDropEvent } from '@app/shared/components/mat-chip-draggable.directive'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-data-keys', |
|||
templateUrl: './data-keys.component.html', |
|||
styleUrls: ['./data-keys.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => DataKeysComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: ErrorStateMatcher, |
|||
useExisting: DataKeysComponent |
|||
} |
|||
] |
|||
}) |
|||
export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges, ErrorStateMatcher { |
|||
|
|||
datasourceTypes = DatasourceType; |
|||
widgetTypes = widgetType; |
|||
dataKeyTypes = DataKeyType; |
|||
|
|||
keysListFormGroup: FormGroup; |
|||
|
|||
modelValue: Array<DataKey> | null; |
|||
|
|||
@Input() |
|||
widgetType: widgetType; |
|||
|
|||
@Input() |
|||
datasourceType: DatasourceType; |
|||
|
|||
@Input() |
|||
maxDataKeys: number; |
|||
|
|||
@Input() |
|||
optDataKeys: boolean; |
|||
|
|||
@Input() |
|||
aliasController: IAliasController; |
|||
|
|||
@Input() |
|||
datakeySettingsSchema: any; |
|||
|
|||
@Input() |
|||
callbacks: DataKeysCallbacks; |
|||
|
|||
@Input() |
|||
entityAliasId: string; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue || !this.optDataKeys; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@ViewChild('keyInput', {static: false}) keyInput: ElementRef<HTMLInputElement>; |
|||
@ViewChild('keyAutocomplete', {static: false}) matAutocomplete: MatAutocomplete; |
|||
@ViewChild('chipList', {static: false}) chipList: MatChipList; |
|||
|
|||
keys: Array<DataKey> = []; |
|||
filteredKeys: Observable<Array<DataKey>>; |
|||
|
|||
separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON]; |
|||
|
|||
alarmKeys: Array<DataKey>; |
|||
functionTypeKeys: Array<DataKey>; |
|||
|
|||
dataKeyType: DataKeyType; |
|||
placeholder: string; |
|||
requiredText: string; |
|||
|
|||
private searchText = ''; |
|||
private latestSearchTextResult: Array<DataKey> = null; |
|||
|
|||
private dirty = false; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public translate: TranslateService, |
|||
private utils: UtilsService, |
|||
private dialogs: DialogService, |
|||
private dialog: MatDialog, |
|||
private fb: FormBuilder, |
|||
public truncate: TruncatePipe) { |
|||
this.keysListFormGroup = this.fb.group({ |
|||
keys: [null, this.required ? [Validators.required] : []], |
|||
key: [null] |
|||
}); |
|||
} |
|||
|
|||
updateValidators() { |
|||
this.keysListFormGroup.get('keys').setValidators(this.required ? [Validators.required] : []); |
|||
this.keysListFormGroup.get('keys').updateValueAndValidity(); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.alarmKeys = []; |
|||
for (const name of Object.keys(alarmFields)) { |
|||
this.alarmKeys.push({ |
|||
name, |
|||
type: DataKeyType.alarm |
|||
}); |
|||
} |
|||
this.functionTypeKeys = []; |
|||
for (const type of this.utils.getPredefinedFunctionsList()) { |
|||
this.functionTypeKeys.push({ |
|||
name: type, |
|||
type: DataKeyType.function |
|||
}); |
|||
} |
|||
this.updateParams(); |
|||
this.filteredKeys = this.keysListFormGroup.get('key').valueChanges |
|||
.pipe( |
|||
tap((value: string | DataKey) => { |
|||
if (value && typeof value !== 'string') { |
|||
this.addFromChipValue(value); |
|||
} else if (value === null) { |
|||
this.clear(this.keyInput.nativeElement.value); |
|||
} |
|||
}), |
|||
filter((value) => typeof value === 'string'), |
|||
map((value) => value ? (typeof value === 'string' ? value : value.name) : ''), |
|||
mergeMap(name => this.fetchKeys(name) ), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
public maxDataKeysText(): string { |
|||
if (this.maxDataKeys !== null && this.maxDataKeys > -1) { |
|||
if (this.datasourceType === DatasourceType.function) { |
|||
return this.translate.instant('datakey.maximum-function-types', {count: this.maxDataKeys}); |
|||
} else { |
|||
return this.translate.instant('datakey.maximum-timeseries-or-attributes', {count: this.maxDataKeys}); |
|||
} |
|||
} else { |
|||
return ''; |
|||
} |
|||
} |
|||
|
|||
private updateParams() { |
|||
if (this.widgetType === widgetType.alarm) { |
|||
this.dataKeyType = DataKeyType.alarm; |
|||
this.placeholder = this.translate.instant('datakey.alarm'); |
|||
this.requiredText = this.translate.instant('datakey.alarm-fields-required'); |
|||
} else { |
|||
if (this.datasourceType === DatasourceType.function) { |
|||
this.dataKeyType = DataKeyType.function; |
|||
this.placeholder = this.translate.instant('datakey.function-types'); |
|||
this.requiredText = this.translate.instant('datakey.function-types-required'); |
|||
} else { |
|||
if (this.widgetType === widgetType.latest) { |
|||
this.dataKeyType = null; |
|||
this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required'); |
|||
} else { |
|||
this.dataKeyType = DataKeyType.timeseries; |
|||
this.requiredText = this.translate.instant('datakey.timeseries-required'); |
|||
} |
|||
this.placeholder = ''; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private reset() { |
|||
if (this.widgetType === widgetType.alarm) { |
|||
this.keys = this.utils.getDefaultAlarmDataKeys(); |
|||
} else { |
|||
this.keys = []; |
|||
} |
|||
this.keysListFormGroup.get('keys').setValue(this.keys); |
|||
this.modelValue = this.keys.length ? [...this.keys] : null; |
|||
if (this.keyInput) { |
|||
this.keyInput.nativeElement.value = ''; |
|||
} |
|||
this.keysListFormGroup.get('key').patchValue('', {emitEvent: false}); |
|||
this.propagateChange(this.modelValue); |
|||
this.dirty = true; |
|||
this.latestSearchTextResult = null; |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (!change.firstChange && change.currentValue !== change.previousValue) { |
|||
if (propName === 'entityAliasId') { |
|||
this.searchText = ''; |
|||
this.latestSearchTextResult = null; |
|||
this.dirty = true; |
|||
} else if (['widgetType', 'datasourceType'].includes(propName)) { |
|||
this.updateParams(); |
|||
setTimeout(() => { |
|||
this.reset(); |
|||
}, 1); |
|||
} else if (['required', 'optDataKeys'].includes('propName')) { |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
|||
const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
|||
const customErrorState = this.required && !this.modelValue; |
|||
return originalErrorState || customErrorState; |
|||
} |
|||
|
|||
ngAfterViewInit(): void {} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.keysListFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.keysListFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: Array<DataKey> | null): void { |
|||
this.searchText = ''; |
|||
if (value != null && value.length) { |
|||
this.modelValue = [...value]; |
|||
this.keys = [...value]; |
|||
this.keysListFormGroup.get('keys').setValue(this.keys); |
|||
} else { |
|||
this.keys = []; |
|||
this.keysListFormGroup.get('keys').setValue(this.keys); |
|||
this.modelValue = null; |
|||
} |
|||
this.dirty = true; |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.keysListFormGroup.get('key').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
private addFromChipValue(chip: DataKey) { |
|||
const key = this.callbacks.generateDataKey(chip.name, chip.type); |
|||
this.addKey(key); |
|||
} |
|||
|
|||
addKey(key: DataKey): void { |
|||
if (!this.maxDataKeys || this.maxDataKeys < 0 || |
|||
!this.modelValue || this.modelValue.length < this.maxDataKeys) { |
|||
if (!this.modelValue) { |
|||
this.modelValue = []; |
|||
} |
|||
this.modelValue.push(key); |
|||
this.keys.push(key); |
|||
this.keysListFormGroup.get('keys').setValue(this.keys); |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
this.clear(); |
|||
} |
|||
|
|||
add(event: MatChipInputEvent): void { |
|||
const value = event.value; |
|||
if ((value || '').trim()) { |
|||
if (this.dataKeyType) { |
|||
this.addFromChipValue({name: value.trim(), type: this.dataKeyType}); |
|||
} |
|||
} |
|||
this.clear(); |
|||
} |
|||
|
|||
remove(key: DataKey) { |
|||
const index = this.keys.indexOf(key); |
|||
if (index >= 0) { |
|||
this.keys.splice(index, 1); |
|||
this.keysListFormGroup.get('keys').setValue(this.keys); |
|||
this.modelValue.splice(index, 1); |
|||
if (!this.modelValue.length) { |
|||
this.modelValue = null; |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
this.clear(); |
|||
} |
|||
} |
|||
|
|||
onChipDrop(event: MatChipDropEvent) { |
|||
const from = event.from; |
|||
const to = event.to; |
|||
this.keys.splice(to, 0, this.keys.splice(from, 1)[0]); |
|||
this.keysListFormGroup.get('keys').setValue(this.keys); |
|||
this.modelValue.splice(to, 0, this.modelValue.splice(from, 1)[0]); |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
|
|||
showColorPicker(key: DataKey) { |
|||
this.dialogs.colorPicker(key.color).subscribe( |
|||
(color) => { |
|||
if (color && key.color !== color) { |
|||
key.color = color; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
editDataKey(key: DataKey, index: number) { |
|||
this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent, |
|||
{ |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
dataKey: deepClone(key), |
|||
dataKeySettingsSchema: this.datakeySettingsSchema, |
|||
entityAliasId: this.entityAliasId, |
|||
callbacks: this.callbacks |
|||
} |
|||
}).afterClosed().subscribe((updatedDataKey) => { |
|||
if (updatedDataKey) { |
|||
this.keys[index] = updatedDataKey; |
|||
this.keysListFormGroup.get('keys').setValue(this.keys); |
|||
this.modelValue[index] = updatedDataKey; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
createKey(name: string, dataKeyType: DataKeyType = this.dataKeyType) { |
|||
this.addFromChipValue({name, type: dataKeyType}); |
|||
} |
|||
|
|||
displayKeyFn(key?: DataKey): string | undefined { |
|||
return key ? key.name : undefined; |
|||
} |
|||
|
|||
fetchKeys(searchText?: string): Observable<Array<DataKey>> { |
|||
if (this.latestSearchTextResult === null || this.searchText !== searchText) { |
|||
this.searchText = searchText; |
|||
let fetchObservable: Observable<Array<DataKey>> = null; |
|||
if (this.datasourceType === DatasourceType.function || this.widgetType === widgetType.alarm) { |
|||
const dataKeyFilter = this.createDataKeyFilter(this.searchText); |
|||
const targetKeysList = this.datasourceType === DatasourceType.function ? this.functionTypeKeys : this.alarmKeys; |
|||
fetchObservable = of(targetKeysList.filter(dataKeyFilter)); |
|||
} else { |
|||
if (this.entityAliasId) { |
|||
const dataKeyTypes = [DataKeyType.timeseries]; |
|||
if (this.widgetType === widgetType.latest) { |
|||
dataKeyTypes.push(DataKeyType.attribute); |
|||
} |
|||
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, this.searchText, dataKeyTypes); |
|||
} else { |
|||
fetchObservable = of([]); |
|||
} |
|||
} |
|||
return fetchObservable.pipe( |
|||
tap(res => this.latestSearchTextResult = res) |
|||
); |
|||
} |
|||
return of(this.latestSearchTextResult); |
|||
} |
|||
|
|||
private createDataKeyFilter(query: string): (key: DataKey) => boolean { |
|||
const lowercaseQuery = query.toLowerCase(); |
|||
return key => key.name.toLowerCase().indexOf(lowercaseQuery) === 0; |
|||
} |
|||
|
|||
textIsNotEmpty(text: string): boolean { |
|||
return (text && text != null && text.length > 0) ? true : false; |
|||
} |
|||
|
|||
clear(value: string = '') { |
|||
this.keyInput.nativeElement.value = value; |
|||
this.keysListFormGroup.get('key').patchValue(value, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.keyInput.nativeElement.blur(); |
|||
this.keyInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
} |
|||
@ -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. |
|||
|
|||
--> |
|||
<mat-form-field [formGroup]="colorFormGroup"> |
|||
<mat-label> |
|||
<mat-icon *ngIf="icon">{{icon}}</mat-icon> |
|||
<span *ngIf="label">{{label}}</span> |
|||
</mat-label> |
|||
<div matPrefix class="tb-color-preview" (click)="showColorPicker()" style="margin-right: 5px;"> |
|||
<div class="tb-color-result" [ngStyle]="{background: colorFormGroup.get('color').value}"></div> |
|||
</div> |
|||
<input matInput formControlName="color" (mousedown)="openOnInput && showColorPicker()" [required]="required"> |
|||
<button *ngIf="colorClearButton" |
|||
type="button" |
|||
matSuffix mat-button mat-icon-button aria-label="Clear" |
|||
(click)="clear()"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
<mat-error *ngIf="colorFormGroup.get('color').hasError('required')"> |
|||
{{ requiredText }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
@ -0,0 +1,49 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@mixin tb-checkered-bg() { |
|||
background-color: #fff; |
|||
background-image: |
|||
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd), |
|||
linear-gradient(45deg, #ddd 25%, transparent 25%, transparent 75%, #ddd 75%, #ddd); |
|||
background-position: 0 0, 4px 4px; |
|||
background-size: 8px 8px; |
|||
} |
|||
|
|||
:host { |
|||
.mat-form-field { |
|||
width: 100%; |
|||
} |
|||
.tb-color-preview { |
|||
cursor: pointer; |
|||
box-sizing: border-box; |
|||
position: relative; |
|||
width: 24px; |
|||
min-width: 24px; |
|||
height: 24px; |
|||
overflow: hidden; |
|||
content: ""; |
|||
border: 2px solid #fff; |
|||
border-radius: 50%; |
|||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .14), 0 2px 2px 0 rgba(0, 0, 0, .098), 0 1px 5px 0 rgba(0, 0, 0, .084); |
|||
|
|||
@include tb-checkered-bg(); |
|||
|
|||
.tb-color-result { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
///
|
|||
/// 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, OnInit, ViewChild } from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { DataKey, DatasourceType } from '@shared/models/widget.models'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
Validator, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
import { DataKeysCallbacks } from '@home/components/widget/data-keys.component.models'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { map, mergeMap, tap } from 'rxjs/operators'; |
|||
import { alarmFields } from '@shared/models/alarm.models'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-color-input', |
|||
templateUrl: './color-input.component.html', |
|||
styleUrls: ['./color-input.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ColorInputComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class ColorInputComponent extends PageComponent implements OnInit, ControlValueAccessor { |
|||
|
|||
@Input() |
|||
icon: string; |
|||
|
|||
@Input() |
|||
label: string; |
|||
|
|||
@Input() |
|||
requiredText: string; |
|||
|
|||
private colorClearButtonValue: boolean; |
|||
get colorClearButton(): boolean { |
|||
return this.colorClearButtonValue; |
|||
} |
|||
@Input() |
|||
set colorClearButton(value: boolean) { |
|||
const newVal = coerceBooleanProperty(value); |
|||
if (this.colorClearButtonValue !== newVal) { |
|||
this.colorClearButtonValue = newVal; |
|||
} |
|||
} |
|||
|
|||
private openOnInputValue: boolean; |
|||
get openOnInput(): boolean { |
|||
return this.openOnInputValue; |
|||
} |
|||
@Input() |
|||
set openOnInput(value: boolean) { |
|||
const newVal = coerceBooleanProperty(value); |
|||
if (this.openOnInputValue !== newVal) { |
|||
this.openOnInputValue = newVal; |
|||
} |
|||
} |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
const newVal = coerceBooleanProperty(value); |
|||
if (this.requiredValue !== newVal) { |
|||
this.requiredValue = newVal; |
|||
this.updateValidators(); |
|||
} |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
private modelValue: string; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
public colorFormGroup: FormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private dialogs: DialogService, |
|||
private translate: TranslateService, |
|||
private fb: FormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.colorFormGroup = this.fb.group({ |
|||
color: [null, this.required ? [Validators.required] : []] |
|||
}); |
|||
|
|||
this.colorFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
updateValidators() { |
|||
if (this.colorFormGroup) { |
|||
this.colorFormGroup.get('color').setValidators(this.required ? [Validators.required] : []); |
|||
this.colorFormGroup.get('color').updateValueAndValidity(); |
|||
} |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.colorFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.colorFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: string): void { |
|||
this.modelValue = value; |
|||
this.colorFormGroup.patchValue( |
|||
{ color: this.modelValue }, {emitEvent: false} |
|||
); |
|||
} |
|||
|
|||
private updateModel() { |
|||
const color: string = this.colorFormGroup.get('color').value; |
|||
if (this.modelValue !== color) { |
|||
this.modelValue = color; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
|||
showColorPicker() { |
|||
this.dialogs.colorPicker(this.colorFormGroup.get('color').value).subscribe( |
|||
(color) => { |
|||
if (color) { |
|||
this.colorFormGroup.patchValue( |
|||
{color}, {emitEvent: true} |
|||
); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
clear() { |
|||
this.colorFormGroup.get('color').patchValue(null, {emitEvent: true}); |
|||
} |
|||
} |
|||
@ -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. |
|||
|
|||
--> |
|||
<form [formGroup]="colorPickerFormGroup" (ngSubmit)="select()"> |
|||
<div mat-dialog-content fxLayout="row" fxLayoutAlign="center"> |
|||
<span [style.background]="colorPickerFormGroup.get('color').value" |
|||
[cpToggle]="true" |
|||
[cpDialogDisplay]="'inline'" |
|||
[colorPicker]="colorPickerFormGroup.get('color').value" |
|||
(colorPickerChange)="onColorChange($event)"> |
|||
</span> |
|||
</div> |
|||
<div mat-dialog-actions fxLayout="row"> |
|||
<span fxFlex></span> |
|||
<button mat-button |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-button |
|||
type="submit" |
|||
style="margin-right: 20px;" |
|||
[disabled]="(isLoading$ | async) || colorPickerFormGroup.invalid || !colorPickerFormGroup.dirty"> |
|||
{{ 'action.select' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,89 @@ |
|||
///
|
|||
/// 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, ViewChild } from '@angular/core'; |
|||
import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; |
|||
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 { forkJoin, Observable } from 'rxjs'; |
|||
import { JsonObjectEditComponent } from '@app/shared/components/json-object-edit.component'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
|||
import { AttributeService } from '@core/http/attribute.service'; |
|||
|
|||
export interface ColorPickerDialogData { |
|||
color: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-color-picker-dialog', |
|||
templateUrl: './color-picker-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: ColorPickerDialogComponent}], |
|||
styleUrls: [] |
|||
}) |
|||
export class ColorPickerDialogComponent extends DialogComponent<ColorPickerDialogComponent, string> |
|||
implements OnInit, ErrorStateMatcher { |
|||
|
|||
colorPickerFormGroup: FormGroup; |
|||
|
|||
submitted = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: ColorPickerDialogData, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
|||
public dialogRef: MatDialogRef<ColorPickerDialogComponent, string>, |
|||
public fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.colorPickerFormGroup = this.fb.group({ |
|||
color: [this.data.color, [Validators.required]] |
|||
}); |
|||
} |
|||
|
|||
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; |
|||
} |
|||
|
|||
onColorChange(color: string) { |
|||
this.colorPickerFormGroup.get('color').setValue(color); |
|||
this.colorPickerFormGroup.markAsDirty(); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
select(): void { |
|||
this.submitted = true; |
|||
const color: string = this.colorPickerFormGroup.get('color').value; |
|||
this.dialogRef.close(color); |
|||
} |
|||
} |
|||
@ -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 class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}" |
|||
tb-fullscreen |
|||
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-js-func-toolbar"> |
|||
<label class="tb-title no-padding">{{'function ' + (functionName ? functionName : '') + '(' + functionArgsString + ') {'}}</label> |
|||
<span fxFlex></span> |
|||
<button type='button' *ngIf="!disabled" mat-button class="tidy" (click)="beautifyJs()"> |
|||
{{'js-func.tidy' | translate }} |
|||
</button> |
|||
<button type='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-javascript-panel" class="tb-js-func-panel" fxLayout="column" tb-toast toastTarget="jsFuncEditor"> |
|||
<div fxFlex #javascriptEditor id="tb-javascript-input" [ngClass]="{'fill-height': fillHeight}"></div> |
|||
</div> |
|||
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;"> |
|||
<label class="tb-title no-padding">}</label> |
|||
</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. |
|||
*/ |
|||
.tb-js-func { |
|||
position: relative; |
|||
|
|||
.tb-disabled { |
|||
color: rgba(0, 0, 0, .38); |
|||
} |
|||
|
|||
.fill-height { |
|||
height: 100%; |
|||
} |
|||
|
|||
.tb-js-func-panel { |
|||
height: calc(100% - 80px); |
|||
margin-left: 15px; |
|||
border: 1px solid #c0c0c0; |
|||
|
|||
#tb-javascript-input { |
|||
width: 100%; |
|||
min-width: 200px; |
|||
height: 100%; |
|||
|
|||
&:not(.fill-height) { |
|||
min-height: 200px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-js-func-toolbar { |
|||
.mat-button.tidy { |
|||
min-width: 32px; |
|||
min-height: 15px; |
|||
padding: 4px; |
|||
margin: 0 5px 0 0; |
|||
font-size: .8rem; |
|||
line-height: 15px; |
|||
color: #7b7b7b; |
|||
background: rgba(220, 220, 220, .35); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,309 @@ |
|||
///
|
|||
/// 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, OnInit, ViewChild, ViewEncapsulation } 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 { UtilsService } from '@core/services/utils.service'; |
|||
import { isUndefined } from '@app/core/utils'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-js-func', |
|||
templateUrl: './js-func.component.html', |
|||
styleUrls: ['./js-func.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => JsFuncComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => JsFuncComponent), |
|||
multi: true, |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator { |
|||
|
|||
@ViewChild('javascriptEditor', {static: true}) |
|||
javascriptEditorElmRef: ElementRef; |
|||
|
|||
private jsEditor: ace.Ace.Editor; |
|||
|
|||
@Input() functionName: string; |
|||
|
|||
@Input() functionArgs: Array<string>; |
|||
|
|||
@Input() validationArgs: Array<string>; |
|||
|
|||
@Input() resultType: string; |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() fillHeight: boolean; |
|||
|
|||
private noValidateValue: boolean; |
|||
get noValidate(): boolean { |
|||
return this.noValidateValue; |
|||
} |
|||
@Input() |
|||
set noValidate(value: boolean) { |
|||
this.noValidateValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
functionArgsString = ''; |
|||
|
|||
fullscreen = false; |
|||
|
|||
modelValue: string; |
|||
|
|||
functionValid = true; |
|||
|
|||
validationError: string; |
|||
|
|||
errorShowed = false; |
|||
|
|||
errorMarkers: number[] = []; |
|||
errorAnnotationId = -1; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor(public elementRef: ElementRef, |
|||
private utils: UtilsService, |
|||
private translate: TranslateService, |
|||
protected store: Store<AppState>) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
if (!this.resultType || this.resultType.length === 0) { |
|||
this.resultType = 'nocheck'; |
|||
} |
|||
if (this.functionArgs) { |
|||
this.functionArgs.forEach((functionArg) => { |
|||
if (this.functionArgsString.length > 0) { |
|||
this.functionArgsString += ', '; |
|||
} |
|||
this.functionArgsString += functionArg; |
|||
}); |
|||
} |
|||
const editorElement = this.javascriptEditorElmRef.nativeElement; |
|||
let editorOptions: Partial<ace.Ace.EditorOptions> = { |
|||
mode: 'ace/mode/javascript', |
|||
showGutter: true, |
|||
showPrintMargin: true, |
|||
readOnly: this.disabled |
|||
}; |
|||
|
|||
const advancedOptions = { |
|||
enableSnippets: true, |
|||
enableBasicAutocompletion: true, |
|||
enableLiveAutocompletion: true |
|||
}; |
|||
|
|||
editorOptions = {...editorOptions, ...advancedOptions}; |
|||
this.jsEditor = ace.edit(editorElement, editorOptions); |
|||
this.jsEditor.session.setUseWrapMode(true); |
|||
this.jsEditor.setValue(this.modelValue ? this.modelValue : '', -1); |
|||
this.jsEditor.on('change', () => { |
|||
this.cleanupJsErrors(); |
|||
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.functionValid) ? null : { |
|||
jsFunc: { |
|||
valid: false, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
beautifyJs() { |
|||
const res = js_beautify(this.modelValue, {indent_size: 4, wrap_line_length: 60}); |
|||
this.jsEditor.setValue(res ? res : '', -1); |
|||
this.updateView(); |
|||
} |
|||
|
|||
validateOnSubmit(): void { |
|||
if (!this.disabled) { |
|||
this.cleanupJsErrors(); |
|||
this.functionValid = this.validateJsFunc(); |
|||
if (!this.functionValid) { |
|||
this.propagateChange(this.modelValue); |
|||
this.store.dispatch(new ActionNotificationShow( |
|||
{ |
|||
message: this.validationError, |
|||
type: 'error', |
|||
target: 'jsFuncEditor', |
|||
verticalPosition: 'bottom', |
|||
horizontalPosition: 'left' |
|||
})); |
|||
this.errorShowed = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private validateJsFunc(): boolean { |
|||
try { |
|||
const toValidate = new Function(this.functionArgsString, this.modelValue); |
|||
if (this.noValidate) { |
|||
return true; |
|||
} |
|||
if (this.validationArgs) { |
|||
let res: any; |
|||
let validationError: any; |
|||
for (const validationArg of this.validationArgs) { |
|||
try { |
|||
res = toValidate.apply(this, validationArg); |
|||
validationError = null; |
|||
break; |
|||
} catch (e) { |
|||
validationError = e; |
|||
} |
|||
} |
|||
if (validationError) { |
|||
throw validationError; |
|||
} |
|||
if (this.resultType !== 'nocheck') { |
|||
if (this.resultType === 'any') { |
|||
if (isUndefined(res)) { |
|||
this.validationError = this.translate.instant('js-func.no-return-error'); |
|||
return false; |
|||
} |
|||
} else { |
|||
const resType = typeof res; |
|||
if (resType !== this.resultType) { |
|||
this.validationError = this.translate.instant('js-func.return-type-mismatch', {type: this.resultType}); |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
return true; |
|||
} else { |
|||
return true; |
|||
} |
|||
} catch (e) { |
|||
const details = this.utils.parseException(e); |
|||
let errorInfo = 'Error:'; |
|||
if (details.name) { |
|||
errorInfo += ' ' + details.name + ':'; |
|||
} |
|||
if (details.message) { |
|||
errorInfo += ' ' + details.message; |
|||
} |
|||
if (details.lineNumber) { |
|||
errorInfo += '<br>Line ' + details.lineNumber; |
|||
if (details.columnNumber) { |
|||
errorInfo += ' column ' + details.columnNumber; |
|||
} |
|||
errorInfo += ' of script.'; |
|||
} |
|||
this.validationError = errorInfo; |
|||
if (details.lineNumber) { |
|||
const line = details.lineNumber - 1; |
|||
let column = 0; |
|||
if (details.columnNumber) { |
|||
column = details.columnNumber; |
|||
} |
|||
const errorMarkerId = this.jsEditor.session.addMarker(new ace.Range(line, 0, line, Infinity), |
|||
'ace_active-line', 'screenLine'); |
|||
this.errorMarkers.push(errorMarkerId); |
|||
const annotations = this.jsEditor.session.getAnnotations(); |
|||
const errorAnnotation: ace.Ace.Annotation = { |
|||
row: line, |
|||
column, |
|||
text: details.message, |
|||
type: 'error' |
|||
}; |
|||
this.errorAnnotationId = annotations.push(errorAnnotation) - 1; |
|||
this.jsEditor.session.setAnnotations(annotations); |
|||
} |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
private cleanupJsErrors(): void { |
|||
if (this.errorShowed) { |
|||
this.store.dispatch(new ActionNotificationHide( |
|||
{ |
|||
target: 'jsFuncEditor' |
|||
})); |
|||
this.errorShowed = false; |
|||
} |
|||
this.errorMarkers.forEach((errorMarker) => { |
|||
this.jsEditor.session.removeMarker(errorMarker); |
|||
}); |
|||
this.errorMarkers.length = 0; |
|||
if (this.errorAnnotationId > -1) { |
|||
const annotations = this.jsEditor.session.getAnnotations(); |
|||
annotations.splice(this.errorAnnotationId, 1); |
|||
this.jsEditor.session.setAnnotations(annotations); |
|||
this.errorAnnotationId = -1; |
|||
} |
|||
} |
|||
|
|||
writeValue(value: string): void { |
|||
this.modelValue = value; |
|||
if (this.jsEditor) { |
|||
this.jsEditor.setValue(this.modelValue ? this.modelValue : '', -1); |
|||
} |
|||
} |
|||
|
|||
updateView() { |
|||
const editorValue = this.jsEditor.getValue(); |
|||
if (this.modelValue !== editorValue) { |
|||
this.modelValue = editorValue; |
|||
this.functionValid = true; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
|||
onFullscreen() { |
|||
if (this.jsEditor) { |
|||
setTimeout(() => { |
|||
this.jsEditor.resize(); |
|||
}, 0); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-json-form" style="background: #fff;" tb-fullscreen [fullscreen]="isFullscreen" |
|||
(fullscreenChanged)="onFullscreenChanged()"> |
|||
<div #reactRoot></div> |
|||
<div>{{ model | json }}</div> |
|||
</div> |
|||
@ -0,0 +1,20 @@ |
|||
/** |
|||
* Copyright © 2016-2019 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
.tb-json-form { |
|||
padding: 12px; |
|||
padding-bottom: 14px !important; |
|||
overflow: auto; |
|||
} |
|||
@ -0,0 +1,216 @@ |
|||
///
|
|||
/// 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, |
|||
OnDestroy, |
|||
OnInit, |
|||
SimpleChanges, |
|||
ViewChild, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { deepClone, isString } from '@app/core/utils'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { JsonFormProps } from './react/json-form.models'; |
|||
import inspector from 'schema-inspector'; |
|||
import * as tinycolor from 'tinycolor2'; |
|||
import { DialogService } from '@app/core/services/dialog.service'; |
|||
import * as React from 'react'; |
|||
import * as ReactDOM from 'react-dom'; |
|||
import ReactSchemaForm from './react/json-form-react'; |
|||
import JsonFormUtils from './react/json-form-utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-json-form', |
|||
templateUrl: './json-form.component.html', |
|||
styleUrls: ['./json-form.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => JsonFormComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => JsonFormComponent), |
|||
multi: true, |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class JsonFormComponent implements OnInit, ControlValueAccessor, Validator, OnChanges, OnDestroy { |
|||
|
|||
@ViewChild('reactRoot', {static: true}) |
|||
reactRootElmRef: ElementRef<HTMLElement>; |
|||
|
|||
@Input() schema: any; |
|||
|
|||
@Input() form: any; |
|||
|
|||
@Input() groupInfoes: any[]; |
|||
|
|||
private readonlyValue: boolean; |
|||
get readonly(): boolean { |
|||
return this.readonlyValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.readonlyValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
formProps: JsonFormProps = { |
|||
isFullscreen: false, |
|||
option: { |
|||
formDefaults: { |
|||
startEmpty: true |
|||
} |
|||
}, |
|||
onModelChange: this.onModelChange.bind(this), |
|||
onColorClick: this.onColorClick.bind(this), |
|||
onToggleFullscreen: this.onToggleFullscreen.bind(this) |
|||
}; |
|||
|
|||
model: any; |
|||
|
|||
isModelValid = true; |
|||
|
|||
isFullscreen = false; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor(public elementRef: ElementRef, |
|||
private translate: TranslateService, |
|||
private dialogs: DialogService, |
|||
protected store: Store<AppState>) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
this.destroyReactSchemaForm(); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
} |
|||
|
|||
public validate(c: FormControl) { |
|||
return this.isModelValid ? null : { |
|||
modelValid: false |
|||
}; |
|||
} |
|||
|
|||
writeValue(value: any): void { |
|||
this.model = value || {}; |
|||
this.model = inspector.sanitize(this.schema, this.model).data; |
|||
this.updateAndRender(); |
|||
this.isModelValid = this.validateModel(); |
|||
if (!this.isModelValid) { |
|||
this.updateView(); |
|||
} |
|||
} |
|||
|
|||
updateView() { |
|||
this.propagateChange(this.model); |
|||
} |
|||
|
|||
ngOnChanges(changes: SimpleChanges): void { |
|||
for (const propName of Object.keys(changes)) { |
|||
const change = changes[propName]; |
|||
if (!change.firstChange && change.currentValue !== change.previousValue) { |
|||
if (propName === 'schema') { |
|||
this.model = inspector.sanitize(this.schema, this.model).data; |
|||
this.isModelValid = this.validateModel(); |
|||
} |
|||
if (['readonly', 'schema', 'form', 'groupInfoes'].includes(propName)) { |
|||
this.updateAndRender(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
onFullscreenChanged() {} |
|||
|
|||
private onModelChange(key: string | string[], val: any) { |
|||
if (isString(val) && val === '') { |
|||
val = undefined; |
|||
} |
|||
if (JsonFormUtils.updateValue(key, this.model, val)) { |
|||
this.formProps.model = this.model; |
|||
this.updateView(); |
|||
} |
|||
} |
|||
|
|||
private onColorClick(event: MouseEvent, key: string, val: string) { |
|||
this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => { |
|||
const e = event as any; |
|||
if (e.data && e.data.onValueChanged) { |
|||
e.data.onValueChanged(tinycolor(color).toRgb()); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private onToggleFullscreen() { |
|||
this.isFullscreen = !this.isFullscreen; |
|||
this.formProps.isFullscreen = this.isFullscreen; |
|||
} |
|||
|
|||
private updateAndRender() { |
|||
const schema = this.schema ? deepClone(this.schema) : { |
|||
type: 'object' |
|||
}; |
|||
schema.strict = true; |
|||
const form = this.form ? deepClone(this.form) : [ '*' ]; |
|||
const groupInfoes = this.groupInfoes ? deepClone(this.groupInfoes) : []; |
|||
this.formProps.option.formDefaults.readonly = this.readonly; |
|||
this.formProps.schema = schema; |
|||
this.formProps.form = form; |
|||
this.formProps.groupInfoes = groupInfoes; |
|||
this.formProps.model = deepClone(this.model); |
|||
this.renderReactSchemaForm(); |
|||
} |
|||
|
|||
private renderReactSchemaForm() { |
|||
ReactDOM.render(React.createElement(ReactSchemaForm, this.formProps), this.reactRootElmRef.nativeElement); |
|||
} |
|||
|
|||
private destroyReactSchemaForm() { |
|||
ReactDOM.unmountComponentAtNode(this.reactRootElmRef.nativeElement); |
|||
} |
|||
|
|||
private validateModel(): boolean { |
|||
if (this.schema && this.model) { |
|||
return JsonFormUtils.validateBySchema(this.schema, this.model).valid; |
|||
} |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,118 @@ |
|||
/* |
|||
* 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 * as React from 'react'; |
|||
import JsonFormUtils from './json-form-utils'; |
|||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|||
|
|||
export default ThingsboardBaseComponent => class extends React.Component<JsonFormFieldProps, JsonFormFieldState> { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.onChangeValidate = this.onChangeValidate.bind(this); |
|||
const value = this.defaultValue(); |
|||
const validationResult = JsonFormUtils.validate(this.props.form, value); |
|||
this.state = { |
|||
value, |
|||
valid: !!(validationResult.valid || !value), |
|||
error: !validationResult.valid && value ? validationResult.error.message : null |
|||
}; |
|||
} |
|||
|
|||
componentDidMount() { |
|||
/* if (typeof this.state.value !== 'undefined') { |
|||
this.props.onChange(this.props.form.key, this.state.value); |
|||
}*/ |
|||
} |
|||
|
|||
onChangeValidate(e) { |
|||
let value = null; |
|||
if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') { |
|||
if (e.target.value === null || e.target.value === '') { |
|||
value = undefined; |
|||
} else if (e.target.value.indexOf('.') === -1) { |
|||
value = parseInt(e.target.value, 10); |
|||
} else { |
|||
value = parseFloat(e.target.value); |
|||
} |
|||
} else if (this.props.form.schema.type === 'boolean') { |
|||
value = e.target.checked; |
|||
} else if (this.props.form.schema.type === 'date' || this.props.form.schema.type === 'array') { |
|||
value = e; |
|||
} else { // string
|
|||
value = e.target.value; |
|||
} |
|||
const validationResult = JsonFormUtils.validate(this.props.form, value); |
|||
this.setState({ |
|||
value, |
|||
valid: validationResult.valid, |
|||
error: validationResult.valid ? null : validationResult.error.message |
|||
}); |
|||
this.props.onChange(this.props.form.key, value); |
|||
} |
|||
|
|||
defaultValue() { |
|||
let value = JsonFormUtils.selectOrSet(this.props.form.key, this.props.model); |
|||
if (this.props.form.schema.type === 'boolean') { |
|||
if (typeof value !== 'boolean' && this.props.form.default) { |
|||
value = this.props.form.default; |
|||
} |
|||
if (typeof value !== 'boolean' && this.props.form.schema && this.props.form.schema.default) { |
|||
value = this.props.form.schema.default; |
|||
} |
|||
if (typeof value !== 'boolean' && |
|||
this.props.form.schema && |
|||
this.props.form.required) { |
|||
value = false; |
|||
} |
|||
} else if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') { |
|||
if (typeof value !== 'number' && this.props.form.default) { |
|||
value = this.props.form.default; |
|||
} |
|||
if (typeof value !== 'number' && this.props.form.schema && this.props.form.schema.default) { |
|||
value = this.props.form.schema.default; |
|||
} |
|||
if (typeof value !== 'number' && this.props.form.titleMap && this.props.form.titleMap[0].value) { |
|||
value = this.props.form.titleMap[0].value; |
|||
} |
|||
if (value && typeof value === 'string') { |
|||
if (value.indexOf('.') === -1) { |
|||
value = parseInt(value, 10); |
|||
} else { |
|||
value = parseFloat(value); |
|||
} |
|||
} |
|||
} else { |
|||
if (!value && this.props.form.default) { |
|||
value = this.props.form.default; |
|||
} |
|||
if (!value && this.props.form.schema && this.props.form.schema.default) { |
|||
value = this.props.form.schema.default; |
|||
} |
|||
if (!value && this.props.form.titleMap && this.props.form.titleMap[0].value) { |
|||
value = this.props.form.titleMap[0].value; |
|||
} |
|||
} |
|||
return value; |
|||
} |
|||
|
|||
render() { |
|||
if (this.props.form && this.props.form.schema) { |
|||
return <ThingsboardBaseComponent {...this.props} {...this.state} onChangeValidate={this.onChangeValidate}/>; |
|||
} else { |
|||
return <div></div>; |
|||
} |
|||
} |
|||
}; |
|||
@ -0,0 +1,46 @@ |
|||
/* |
|||
* 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 * as React from 'react'; |
|||
import ThingsboardBaseComponent from './json-form-base-component'; |
|||
import Checkbox from '@material-ui/core/Checkbox'; |
|||
import { JsonFormFieldProps, JsonFormFieldState } from './json-form.models.js'; |
|||
import FormControlLabel from '@material-ui/core/FormControlLabel'; |
|||
|
|||
class ThingsboardCheckbox extends React.Component<JsonFormFieldProps, JsonFormFieldState> { |
|||
render() { |
|||
return ( |
|||
<div> |
|||
<FormControlLabel |
|||
control={ |
|||
<Checkbox |
|||
name={this.props.form.key.slice(-1)[0]} |
|||
value={this.props.form.key.slice(-1)[0]} |
|||
defaultChecked={this.props.value || false} |
|||
disabled={this.props.form.readonly} |
|||
onChange={(e, checked) => { |
|||
this.props.onChangeValidate(e); |
|||
}} |
|||
style={{paddingTop: '14px'}} |
|||
/> |
|||
} |
|||
label={this.props.form.title} |
|||
/> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default ThingsboardBaseComponent(ThingsboardCheckbox); |
|||
@ -0,0 +1,12 @@ |
|||
import * as React from 'react'; |
|||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|||
|
|||
class ThingsboardHelp extends React.Component<JsonFormFieldProps, JsonFormFieldState> { |
|||
render() { |
|||
return ( |
|||
<div className={this.props.form.htmlClass} dangerouslySetInnerHTML={{__html: this.props.form.description}} ></div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default ThingsboardHelp; |
|||
@ -0,0 +1,97 @@ |
|||
/* |
|||
* 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 * as React from 'react'; |
|||
import ThingsboardBaseComponent from './json-form-base-component'; |
|||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|||
import { TextField } from '@material-ui/core'; |
|||
|
|||
interface ThingsboardNumberState extends JsonFormFieldState { |
|||
focused: boolean; |
|||
lastSuccessfulValue: number; |
|||
} |
|||
|
|||
class ThingsboardNumber extends React.Component<JsonFormFieldProps, ThingsboardNumberState> { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.preValidationCheck = this.preValidationCheck.bind(this); |
|||
this.onBlur = this.onBlur.bind(this); |
|||
this.onFocus = this.onFocus.bind(this); |
|||
this.state = { |
|||
lastSuccessfulValue: this.props.value, |
|||
focused: false |
|||
}; |
|||
} |
|||
|
|||
isNumeric(n) { |
|||
return n === null || n === '' || !isNaN(n) && isFinite(n); |
|||
} |
|||
|
|||
onBlur() { |
|||
this.setState({focused: false}); |
|||
} |
|||
|
|||
onFocus() { |
|||
this.setState({focused: true}); |
|||
} |
|||
|
|||
preValidationCheck(e) { |
|||
if (this.isNumeric(e.target.value)) { |
|||
this.setState({ |
|||
lastSuccessfulValue: e.target.value |
|||
}); |
|||
this.props.onChangeValidate(e); |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
|
|||
let fieldClass = 'tb-field'; |
|||
if (this.props.form.required) { |
|||
fieldClass += ' tb-required'; |
|||
} |
|||
if (this.props.form.readonly) { |
|||
fieldClass += ' tb-readonly'; |
|||
} |
|||
if (this.state.focused) { |
|||
fieldClass += ' tb-focused'; |
|||
} |
|||
let value = this.state.lastSuccessfulValue; |
|||
if (typeof value !== 'undefined') { |
|||
value = Number(value); |
|||
} else { |
|||
value = null; |
|||
} |
|||
return ( |
|||
<div> |
|||
<TextField |
|||
className={fieldClass} |
|||
label={this.props.form.title} |
|||
type='number' |
|||
error={!this.props.valid} |
|||
helperText={this.props.valid ? this.props.form.placeholder : this.props.error} |
|||
onChange={this.preValidationCheck} |
|||
defaultValue={value} |
|||
disabled={this.props.form.readonly} |
|||
onFocus={this.onFocus} |
|||
onBlur={this.onBlur} |
|||
style={this.props.form.style || {width: '100%'}}/> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default ThingsboardBaseComponent(ThingsboardNumber); |
|||
@ -0,0 +1,30 @@ |
|||
import * as React from 'react'; |
|||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|||
import FormControlLabel from '@material-ui/core/FormControlLabel'; |
|||
import { Radio, FormLabel, RadioGroup } from '@material-ui/core'; |
|||
import FormControl from '@material-ui/core/FormControl'; |
|||
import ThingsboardBaseComponent from '@shared/components/json-form/react/json-form-base-component'; |
|||
|
|||
class ThingsboardRadios extends React.Component<JsonFormFieldProps, JsonFormFieldState> { |
|||
|
|||
render() { |
|||
const items = this.props.form.titleMap.map((item, index) => { |
|||
return ( |
|||
<FormControlLabel value={item.value} control={<Radio />} label={item.name} key={index} /> |
|||
); |
|||
}); |
|||
|
|||
return ( |
|||
<FormControl component='fieldset' |
|||
className={this.props.form.htmlClass} |
|||
disabled={this.props.form.readonly}> |
|||
<FormLabel component='legend'>{this.props.form.title}</FormLabel> |
|||
<RadioGroup name={this.props.form.title} value={this.props.value} onChange={this.props.onChangeValidate}> |
|||
{items} |
|||
</RadioGroup> |
|||
</FormControl> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default ThingsboardBaseComponent(ThingsboardRadios); |
|||
@ -0,0 +1,186 @@ |
|||
/* |
|||
* 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 * as React from 'react'; |
|||
import ThingsboardBaseComponent from './json-form-base-component'; |
|||
import Select, {Option} from 'rc-select/lib'; |
|||
import { |
|||
JsonFormFieldProps, |
|||
JsonFormFieldState, |
|||
KeyLabelItem |
|||
} from '@shared/components/json-form/react/json-form.models'; |
|||
|
|||
interface ThingsboardRcSelectState extends JsonFormFieldState { |
|||
currentValue: KeyLabelItem | KeyLabelItem[]; |
|||
items: Array<KeyLabelItem>; |
|||
focused: boolean; |
|||
} |
|||
|
|||
class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, ThingsboardRcSelectState> { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.onSelect = this.onSelect.bind(this); |
|||
this.onDeselect = this.onDeselect.bind(this); |
|||
this.onBlur = this.onBlur.bind(this); |
|||
this.onFocus = this.onFocus.bind(this); |
|||
this.state = { |
|||
currentValue: this.keyToCurrentValue(this.props.value, this.props.form.schema.type === 'array'), |
|||
items: this.props.form.items, |
|||
focused: false |
|||
}; |
|||
} |
|||
|
|||
keyToCurrentValue(key: string | string[], isArray: boolean): KeyLabelItem | KeyLabelItem[] { |
|||
let currentValue: KeyLabelItem | KeyLabelItem[] = isArray ? [] : null; |
|||
if (isArray) { |
|||
const keys = key; |
|||
if (keys) { |
|||
(keys as string[]).forEach((keyVal) => { |
|||
(currentValue as KeyLabelItem[]).push({key: keyVal, label: this.labelFromKey(keyVal)}); |
|||
}); |
|||
} |
|||
} else { |
|||
currentValue = {key: key as string, label: this.labelFromKey(key as string)}; |
|||
} |
|||
return currentValue; |
|||
} |
|||
|
|||
labelFromKey(key: string): string { |
|||
let label = key || ''; |
|||
if (key) { |
|||
for (const item of this.props.form.items) { |
|||
if (item.value === key) { |
|||
label = item.label; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
return label; |
|||
} |
|||
|
|||
arrayValues(items: KeyLabelItem[]): string[] { |
|||
const v: string[] = []; |
|||
if (items) { |
|||
items.forEach(item => { |
|||
v.push(item.key); |
|||
}); |
|||
} |
|||
return v; |
|||
} |
|||
|
|||
keyIndex(values: KeyLabelItem[], key: string): number { |
|||
let index = -1; |
|||
if (values) { |
|||
for (let i = 0; i < values.length; i++) { |
|||
if (values[i].key === key) { |
|||
index = i; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
return index; |
|||
} |
|||
|
|||
onSelect(value: KeyLabelItem, option) { |
|||
if (this.props.form.schema.type === 'array') { |
|||
const v = this.state.currentValue as KeyLabelItem[]; |
|||
v.push(this.keyToCurrentValue(value.key, false) as KeyLabelItem); |
|||
this.setState({ |
|||
currentValue: v |
|||
}); |
|||
this.props.onChangeValidate(this.arrayValues(v)); |
|||
} else { |
|||
this.setState({currentValue: this.keyToCurrentValue(value.key, false)}); |
|||
this.props.onChangeValidate({target: {value: value.key}}); |
|||
} |
|||
} |
|||
|
|||
onDeselect(value: KeyLabelItem, option) { |
|||
if (this.props.form.schema.type === 'array') { |
|||
const v = this.state.currentValue as KeyLabelItem[]; |
|||
const index = this.keyIndex(v, value.key); |
|||
if (index > -1) { |
|||
v.splice(index, 1); |
|||
} |
|||
this.setState({ |
|||
currentValue: v |
|||
}); |
|||
this.props.onChangeValidate(this.arrayValues(v)); |
|||
} |
|||
} |
|||
|
|||
onBlur() { |
|||
this.setState({ focused: false }); |
|||
} |
|||
|
|||
onFocus() { |
|||
this.setState({ focused: true }); |
|||
} |
|||
|
|||
render() { |
|||
|
|||
let options: JSX.Element[] = []; |
|||
if (this.state.items && this.state.items.length > 0) { |
|||
options = this.state.items.map((item, idx) => ( |
|||
<Option key={idx} value={item.value}>{item.label}</Option> |
|||
)); |
|||
} |
|||
|
|||
let labelClass = 'tb-label'; |
|||
if (this.props.form.required) { |
|||
labelClass += ' tb-required'; |
|||
} |
|||
if (this.props.form.readonly) { |
|||
labelClass += ' tb-readonly'; |
|||
} |
|||
if (this.state.focused) { |
|||
labelClass += ' tb-focused'; |
|||
} |
|||
|
|||
return ( |
|||
<div className='tb-container'> |
|||
<label className={labelClass}>{this.props.form.title}</label> |
|||
<Select |
|||
className={this.props.form.className} |
|||
dropdownClassName={this.props.form.dropdownClassName} |
|||
dropdownStyle={{...this.props.form.dropdownStyle, ...{zIndex: 100001}}} |
|||
dropdownMenuStyle={this.props.form.dropdownMenuStyle} |
|||
allowClear={this.props.form.allowClear} |
|||
tags={this.props.form.tags} |
|||
maxTagTextLength={this.props.form.maxTagTextLength} |
|||
multiple={this.props.form.multiple} |
|||
combobox={this.props.form.combobox} |
|||
disabled={this.props.form.readonly} |
|||
optionLabelProp='children' |
|||
value={this.state.currentValue} |
|||
labelInValue={true} |
|||
onSelect={this.onSelect} |
|||
onDeselect={this.onDeselect} |
|||
onFocus={this.onFocus} |
|||
onBlur={this.onBlur} |
|||
style={this.props.form.style || {width: '100%'}}> |
|||
{options} |
|||
</Select> |
|||
<div className='json-form-error' |
|||
style={{opacity: this.props.valid ? '0' : '1', |
|||
bottom: '-5px'}}>{this.props.error}</div> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default ThingsboardBaseComponent(ThingsboardRcSelect); |
|||
@ -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. |
|||
*/ |
|||
import './json-form.scss'; |
|||
|
|||
import * as React from 'react'; |
|||
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; |
|||
import thingsboardTheme from './styles/thingsboardTheme'; |
|||
import ThingsboardSchemaForm from './json-form-schema-form'; |
|||
import { JsonFormProps } from './json-form.models'; |
|||
|
|||
const tbTheme = createMuiTheme(thingsboardTheme); |
|||
|
|||
class ReactSchemaForm extends React.Component<JsonFormProps, {}> { |
|||
|
|||
static defaultProps: JsonFormProps; |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
} |
|||
|
|||
render() { |
|||
if (this.props.form.length > 0) { |
|||
return <ThemeProvider theme={tbTheme}><ThingsboardSchemaForm {...this.props} /></ThemeProvider>; |
|||
} else { |
|||
return <div></div>; |
|||
} |
|||
} |
|||
} |
|||
|
|||
ReactSchemaForm.defaultProps = { |
|||
isFullscreen: false, |
|||
schema: {}, |
|||
form: ['*'], |
|||
groupInfoes: [], |
|||
option: { |
|||
formDefaults: { |
|||
startEmpty: true |
|||
} |
|||
} |
|||
}; |
|||
|
|||
export default ReactSchemaForm; |
|||
@ -0,0 +1,183 @@ |
|||
/* |
|||
* 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 * as React from 'react'; |
|||
import JsonFormUtils from './json-form-utils'; |
|||
|
|||
/*import ThingsboardArray from './json-form-array.jsx'; |
|||
import ThingsboardJavaScript from './json-form-javascript.jsx'; |
|||
import ThingsboardJson from './json-form-json.jsx'; |
|||
import ThingsboardHtml from './json-form-html.jsx'; |
|||
import ThingsboardCss from './json-form-css.jsx'; |
|||
import ThingsboardColor from './json-form-color.jsx'*/ |
|||
import ThingsboardRcSelect from './json-form-rc-select'; |
|||
import ThingsboardNumber from './json-form-number'; |
|||
import ThingsboardText from './json-form-text'; |
|||
import ThingsboardSelect from './json-form-select'; |
|||
import ThingsboardRadios from './json-form-radios'; |
|||
/*import ThingsboardDate from './json-form-date.jsx'; |
|||
import ThingsboardImage from './json-form-image.jsx';*/ |
|||
import ThingsboardCheckbox from './json-form-checkbox'; |
|||
import ThingsboardHelp from './json-form-help'; |
|||
/*import ThingsboardFieldSet from './json-form-fieldset.jsx';*/ |
|||
import { JsonFormProps, GroupInfo, JsonFormData } from './json-form.models'; |
|||
|
|||
import _ from 'lodash'; |
|||
|
|||
class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> { |
|||
|
|||
private hasConditions: boolean; |
|||
private mapper: {[type: string]: any}; |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
|
|||
this.mapper = { |
|||
number: ThingsboardNumber, |
|||
text: ThingsboardText, |
|||
password: ThingsboardText, |
|||
textarea: ThingsboardText, |
|||
select: ThingsboardSelect, |
|||
radios: ThingsboardRadios, |
|||
// date: ThingsboardDate,
|
|||
// image: ThingsboardImage,
|
|||
checkbox: ThingsboardCheckbox, |
|||
help: ThingsboardHelp, |
|||
// array: ThingsboardArray,
|
|||
// javascript: ThingsboardJavaScript,
|
|||
// json: ThingsboardJson,
|
|||
// html: ThingsboardHtml,
|
|||
// css: ThingsboardCss,
|
|||
// color: ThingsboardColor,
|
|||
'rc-select': ThingsboardRcSelect, |
|||
// fieldset: ThingsboardFieldSet
|
|||
}; |
|||
|
|||
this.onChange = this.onChange.bind(this); |
|||
this.onColorClick = this.onColorClick.bind(this); |
|||
this.onToggleFullscreen = this.onToggleFullscreen.bind(this); |
|||
this.hasConditions = false; |
|||
} |
|||
|
|||
onChange(key: string | string[], val: any) { |
|||
this.props.onModelChange(key, val); |
|||
if (this.hasConditions) { |
|||
this.forceUpdate(); |
|||
} |
|||
} |
|||
|
|||
onColorClick(event, key, val) { |
|||
this.props.onColorClick(event, key, val); |
|||
} |
|||
|
|||
onToggleFullscreen() { |
|||
this.props.onToggleFullscreen(); |
|||
} |
|||
|
|||
|
|||
builder(form: JsonFormData, |
|||
model: any, |
|||
index: number, |
|||
onChange: (key: string | string[], val: any) => void, |
|||
onColorClick: (event: MouseEvent, key: string, val: string) => void, |
|||
onToggleFullscreen: () => void, |
|||
mapper: {[type: string]: any}): JSX.Element { |
|||
const type = form.type; |
|||
const Field = this.mapper[type]; |
|||
if (!Field) { |
|||
console.log('Invalid field: \"' + form.key[0] + '\"!'); |
|||
return null; |
|||
} |
|||
if (form.condition) { |
|||
this.hasConditions = true; |
|||
// tslint:disable-next-line:no-eval
|
|||
if (eval(form.condition) === false) { |
|||
return null; |
|||
} |
|||
} |
|||
return <Field model={model} form={form} key={index} onChange={onChange} |
|||
onColorClick={onColorClick} |
|||
onToggleFullscreen={onToggleFullscreen} |
|||
mapper={mapper} builder={this.builder}/>; |
|||
} |
|||
|
|||
createSchema(theForm: any[]): JSX.Element { |
|||
const merged = JsonFormUtils.merge(this.props.schema, theForm, this.props.ignore, this.props.option); |
|||
let mapper = this.mapper; |
|||
if (this.props.mapper) { |
|||
mapper = _.merge(this.mapper, this.props.mapper); |
|||
} |
|||
const forms = merged.map(function(form, index) { |
|||
return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onToggleFullscreen, mapper); |
|||
}.bind(this)); |
|||
|
|||
let formClass = 'SchemaForm'; |
|||
if (this.props.isFullscreen) { |
|||
formClass += ' SchemaFormFullscreen'; |
|||
} |
|||
|
|||
return ( |
|||
<div style={{width: '100%'}} className={formClass}>{forms}</div> |
|||
); |
|||
} |
|||
|
|||
render() { |
|||
if (this.props.groupInfoes && this.props.groupInfoes.length > 0) { |
|||
const content: JSX.Element[] = []; |
|||
for (const info of this.props.groupInfoes) { |
|||
const forms = this.createSchema(this.props.form[info.formIndex]); |
|||
const item = <ThingsboardSchemaGroup key={content.length} forms={forms} info={info}></ThingsboardSchemaGroup>; |
|||
content.push(item); |
|||
} |
|||
return (<div>{content}</div>); |
|||
} else { |
|||
return this.createSchema(this.props.form); |
|||
} |
|||
} |
|||
} |
|||
export default ThingsboardSchemaForm; |
|||
|
|||
interface ThingsboardSchemaGroupProps { |
|||
info: GroupInfo; |
|||
forms: JSX.Element; |
|||
} |
|||
|
|||
interface ThingsboardSchemaGroupState { |
|||
showGroup: boolean; |
|||
} |
|||
|
|||
class ThingsboardSchemaGroup extends React.Component<ThingsboardSchemaGroupProps, ThingsboardSchemaGroupState> { |
|||
constructor(props) { |
|||
super(props); |
|||
this.state = { |
|||
showGroup: true |
|||
}; |
|||
} |
|||
|
|||
toogleGroup(index) { |
|||
this.setState({ |
|||
showGroup: !this.state.showGroup |
|||
}); |
|||
} |
|||
|
|||
render() { |
|||
const theCla = 'pull-right fa fa-chevron-down md-toggle-icon' + (this.state.showGroup ? '' : ' tb-toggled'); |
|||
return (<section className='mat-elevation-z1' style={{marginTop: '10px'}}> |
|||
<div className='SchemaGroupname md-button-toggle' |
|||
onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div> |
|||
<div style={{padding: '20px'}} className={this.state.showGroup ? '' : 'invisible'}>{this.props.forms}</div> |
|||
</section>); |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
import * as React from 'react'; |
|||
import { |
|||
JsonFormFieldProps, |
|||
JsonFormFieldState, |
|||
KeyLabelItem |
|||
} from '@shared/components/json-form/react/json-form.models'; |
|||
import MenuItem from '@material-ui/core/MenuItem'; |
|||
import FormControl from '@material-ui/core/FormControl'; |
|||
import InputLabel from '@material-ui/core/InputLabel'; |
|||
import Select from '@material-ui/core/Select'; |
|||
import ThingsboardBaseComponent from '@shared/components/json-form/react/json-form-base-component'; |
|||
|
|||
interface ThingsboardSelectState extends JsonFormFieldState { |
|||
currentValue: any; |
|||
} |
|||
|
|||
class ThingsboardSelect extends React.Component<JsonFormFieldProps, ThingsboardSelectState> { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.onSelected = this.onSelected.bind(this); |
|||
const possibleValue = this.getModelKey(this.props.model, this.props.form.key); |
|||
this.state = { |
|||
currentValue: this.props.model !== undefined && possibleValue ? possibleValue : this.props.form.titleMap != null ? |
|||
this.props.form.titleMap[0].value : '' |
|||
}; |
|||
} |
|||
|
|||
componentWillReceiveProps(nextProps) { |
|||
if (nextProps.model && nextProps.form.key) { |
|||
this.setState({ |
|||
currentValue: this.getModelKey(nextProps.model, nextProps.form.key) |
|||
|| (nextProps.form.titleMap != null ? nextProps.form.titleMap[0].value : '') |
|||
}); |
|||
} |
|||
} |
|||
|
|||
getModelKey(model, key) { |
|||
if (Array.isArray(key)) { |
|||
return key.reduce((cur, nxt) => (cur[nxt] || {}), model); |
|||
} else { |
|||
return model[key]; |
|||
} |
|||
} |
|||
|
|||
onSelected(event: React.ChangeEvent<{ name?: string; value: any }>) { |
|||
|
|||
this.setState({ |
|||
currentValue: event.target.value |
|||
}); |
|||
this.props.onChangeValidate(event); |
|||
} |
|||
|
|||
render() { |
|||
const menuItems = this.props.form.titleMap.map((item, idx) => ( |
|||
<MenuItem key={idx} |
|||
value={item.value}>{item.name}</MenuItem> |
|||
)); |
|||
|
|||
return ( |
|||
<FormControl className={this.props.form.htmlClass} |
|||
disabled={this.props.form.readonly} |
|||
fullWidth={true}> |
|||
<InputLabel htmlFor='select-field'>{this.props.form.title}</InputLabel> |
|||
<Select |
|||
value={this.state.currentValue} |
|||
onChange={this.onSelected}> |
|||
{menuItems} |
|||
</Select> |
|||
</FormControl> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default ThingsboardBaseComponent(ThingsboardSelect); |
|||
@ -0,0 +1,84 @@ |
|||
/* |
|||
* 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 * as React from 'react'; |
|||
import ThingsboardBaseComponent from './json-form-base-component'; |
|||
import TextField from '@material-ui/core/TextField'; |
|||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|||
import { FormControl } from '@material-ui/core'; |
|||
|
|||
interface ThingsboardTextState extends JsonFormFieldState { |
|||
focused: boolean; |
|||
} |
|||
|
|||
class ThingsboardText extends React.Component<JsonFormFieldProps, ThingsboardTextState> { |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
this.onBlur = this.onBlur.bind(this); |
|||
this.onFocus = this.onFocus.bind(this); |
|||
this.state = { |
|||
focused: false |
|||
}; |
|||
} |
|||
|
|||
onBlur() { |
|||
this.setState({focused: false}); |
|||
} |
|||
|
|||
onFocus() { |
|||
this.setState({focused: true}); |
|||
} |
|||
|
|||
render() { |
|||
|
|||
let fieldClass = 'tb-field'; |
|||
if (this.props.form.required) { |
|||
fieldClass += ' tb-required'; |
|||
} |
|||
if (this.props.form.readonly) { |
|||
fieldClass += ' tb-readonly'; |
|||
} |
|||
if (this.state.focused) { |
|||
fieldClass += ' tb-focused'; |
|||
} |
|||
|
|||
const multiline = this.props.form.type === 'textarea'; |
|||
const rows = multiline ? this.props.form.rows : 1; |
|||
const rowsMax = multiline ? this.props.form.rowsMax : 1; |
|||
|
|||
return ( |
|||
<div> |
|||
<TextField |
|||
className={fieldClass} |
|||
type={this.props.form.type} |
|||
label={this.props.form.title} |
|||
multiline={multiline} |
|||
error={!this.props.valid} |
|||
helperText={this.props.valid ? this.props.form.placeholder : this.props.error} |
|||
onChange={this.props.onChangeValidate} |
|||
defaultValue={this.props.value} |
|||
disabled={this.props.form.readonly} |
|||
rows={rows} |
|||
rowsMax={rowsMax} |
|||
onFocus={this.onFocus} |
|||
onBlur={this.onBlur} |
|||
style={this.props.form.style || {width: '100%'}}/> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default ThingsboardBaseComponent(ThingsboardText); |
|||
@ -0,0 +1,542 @@ |
|||
///
|
|||
/// 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 * as tv from 'tv4'; |
|||
import ObjectPath from 'objectpath'; |
|||
import _ from 'lodash'; |
|||
import { SchemaValidationResult, DefaultsFormOptions, FormOption } from './json-form.models'; |
|||
import { isDefined, isString, isUndefined } from '@core/utils'; |
|||
import * as equal from 'deep-equal'; |
|||
|
|||
function validateBySchema(schema: any, value: any): SchemaValidationResult { |
|||
return tv.validateResult(value, schema); |
|||
} |
|||
|
|||
function validate(form: any, value: any): SchemaValidationResult { |
|||
|
|||
if (!form) { |
|||
return {valid: true}; |
|||
} |
|||
const schema = form.schema; |
|||
|
|||
if (!schema) { |
|||
return {valid: true}; |
|||
} |
|||
|
|||
if (value === '') { |
|||
value = undefined; |
|||
} |
|||
|
|||
// Numbers fields will give a null value, which also means empty field
|
|||
if (form.type === 'number' && value === null) { |
|||
value = undefined; |
|||
} |
|||
|
|||
if (form.type === 'number' && isNaN(parseFloat(value))) { |
|||
value = undefined; |
|||
} |
|||
const wrap: any = {type: 'object', properties: {}}; |
|||
const propName = form.key[form.key.length - 1]; |
|||
wrap.properties[propName] = schema; |
|||
|
|||
if (form.required) { |
|||
wrap.required = [propName]; |
|||
} |
|||
const valueWrap = {}; |
|||
if (typeof value !== 'undefined') { |
|||
valueWrap[propName] = value; |
|||
} |
|||
|
|||
const tv4Result: SchemaValidationResult = tv.validateResult(valueWrap, wrap); |
|||
if (tv4Result != null && !tv4Result.valid && form.validationMessage != null && typeof value !== 'undefined') { |
|||
tv4Result.error.message = form.validationMessage; |
|||
} |
|||
return tv4Result; |
|||
} |
|||
|
|||
function stripNullType(type: any): string { |
|||
if (Array.isArray(type) && type.length === 2) { |
|||
if (type[0] === 'null') { |
|||
return type[1]; |
|||
} |
|||
if (type[1] === 'null') { |
|||
return type[0]; |
|||
} |
|||
} |
|||
return type; |
|||
} |
|||
|
|||
const enumToTitleMap = (enm: string[]): { name: string, value: string }[] => { |
|||
const titleMap: { name: string, value: string }[] = []; |
|||
enm.forEach((name) => { |
|||
titleMap.push({name, value: name}); |
|||
}); |
|||
return titleMap; |
|||
}; |
|||
|
|||
const canonicalTitleMap = (titleMap: any, originalEnum?: string[]): { name: string, value: string }[] => { |
|||
if (!_.isArray(titleMap)) { |
|||
const canonical: { name: string, value: string }[] = []; |
|||
if (originalEnum) { |
|||
originalEnum.forEach((value) => { |
|||
canonical.push({name: titleMap[value], value}); |
|||
}); |
|||
} else { |
|||
for (const k of Object.keys(titleMap)) { |
|||
if (titleMap.hasOwnProperty(k)) { |
|||
canonical.push({name: k, value: titleMap[k]}); |
|||
} |
|||
} |
|||
} |
|||
return canonical; |
|||
} |
|||
return titleMap; |
|||
}; |
|||
|
|||
const stdFormObj = (name: string, schema: any, options: DefaultsFormOptions): any => { |
|||
options = options || {}; |
|||
const f = options.global && options.global.formDefaults ? _.cloneDeep(options.global.formDefaults) : {}; |
|||
if (options.global && options.global.supressPropertyTitles === true) { |
|||
f.title = schema.title; |
|||
} else { |
|||
f.title = schema.title || name; |
|||
} |
|||
|
|||
if (schema.description) { |
|||
f.description = schema.description; |
|||
} |
|||
if (options.required === true || schema.required === true) { |
|||
f.required = true; |
|||
} |
|||
if (schema.maxLength) { |
|||
f.maxlength = schema.maxLength; |
|||
} |
|||
if (schema.minLength) { |
|||
f.minlength = schema.minLength; |
|||
} |
|||
if (schema.readOnly || schema.readonly) { |
|||
f.readonly = true; |
|||
} |
|||
if (schema.minimum) { |
|||
f.minimum = schema.minimum + (schema.exclusiveMinimum ? 1 : 0); |
|||
} |
|||
if (schema.maximum) { |
|||
f.maximum = schema.maximum - (schema.exclusiveMaximum ? 1 : 0); |
|||
} |
|||
|
|||
// Non standard attributes (DONT USE DEPRECATED)
|
|||
// If you must set stuff like this in the schema use the x-schema-form attribute
|
|||
if (schema.validationMessage) { |
|||
f.validationMessage = schema.validationMessage; |
|||
} |
|||
if (schema.enumNames) { |
|||
f.titleMap = canonicalTitleMap(schema.enumNames, schema.enum); |
|||
} |
|||
f.schema = schema; |
|||
|
|||
// Ng model options doesn't play nice with undefined, might be defined
|
|||
// globally though
|
|||
f.ngModelOptions = f.ngModelOptions || {}; |
|||
|
|||
return f; |
|||
}; |
|||
|
|||
const text = (name: string, schema: any, options: DefaultsFormOptions): any => { |
|||
if (stripNullType(schema.type) === 'string' && !schema.enum) { |
|||
const f = stdFormObj(name, schema, options); |
|||
f.key = options.path; |
|||
f.type = 'text'; |
|||
options.lookup[ObjectPath.stringify(options.path)] = f; |
|||
return f; |
|||
} |
|||
}; |
|||
|
|||
const numberType = (name: string, schema: any, options: DefaultsFormOptions): any => { |
|||
if (stripNullType(schema.type) === 'number') { |
|||
const f = stdFormObj(name, schema, options); |
|||
f.key = options.path; |
|||
f.type = 'number'; |
|||
options.lookup[ObjectPath.stringify(options.path)] = f; |
|||
return f; |
|||
} |
|||
}; |
|||
|
|||
const integer = (name: string, schema: any, options: DefaultsFormOptions): any => { |
|||
if (stripNullType(schema.type) === 'integer') { |
|||
const f = stdFormObj(name, schema, options); |
|||
f.key = options.path; |
|||
f.type = 'number'; |
|||
options.lookup[ObjectPath.stringify(options.path)] = f; |
|||
return f; |
|||
} |
|||
}; |
|||
|
|||
const date = (name: string, schema: any, options: DefaultsFormOptions): any => { |
|||
if (stripNullType(schema.type) === 'date') { |
|||
const f = stdFormObj(name, schema, options); |
|||
f.key = options.path; |
|||
f.type = 'date'; |
|||
options.lookup[ObjectPath.stringify(options.path)] = f; |
|||
return f; |
|||
} |
|||
}; |
|||
|
|||
const checkbox = (name: string, schema: any, options: DefaultsFormOptions): any => { |
|||
if (stripNullType(schema.type) === 'boolean') { |
|||
const f = stdFormObj(name, schema, options); |
|||
f.key = options.path; |
|||
f.type = 'checkbox'; |
|||
options.lookup[ObjectPath.stringify(options.path)] = f; |
|||
return f; |
|||
} |
|||
}; |
|||
|
|||
const select = (name: string, schema: any, options: DefaultsFormOptions): any => { |
|||
if (stripNullType(schema.type) === 'string' && schema.enum) { |
|||
const f = stdFormObj(name, schema, options); |
|||
f.key = options.path; |
|||
f.type = 'select'; |
|||
if (!f.titleMap) { |
|||
f.titleMap = enumToTitleMap(schema.enum); |
|||
} |
|||
options.lookup[ObjectPath.stringify(options.path)] = f; |
|||
return f; |
|||
} |
|||
}; |
|||
|
|||
const checkboxes = (name: string, schema: any, options: DefaultsFormOptions): any => { |
|||
if (stripNullType(schema.type) === 'array' && schema.items && schema.items.enum) { |
|||
const f = stdFormObj(name, schema, options); |
|||
f.key = options.path; |
|||
f.type = 'checkboxes'; |
|||
if (!f.titleMap) { |
|||
f.titleMap = enumToTitleMap(schema.items.enum); |
|||
} |
|||
options.lookup[ObjectPath.stringify(options.path)] = f; |
|||
return f; |
|||
} |
|||
}; |
|||
|
|||
const fieldset = (name: string, schema: any, options: DefaultsFormOptions): any => { |
|||
if (stripNullType(schema.type) === 'object') { |
|||
const f = stdFormObj(name, schema, options); |
|||
f.type = 'fieldset'; |
|||
f.items = []; |
|||
options.lookup[ObjectPath.stringify(options.path)] = f; |
|||
|
|||
// recurse down into properties
|
|||
for (const k of Object.keys(schema.properties)) { |
|||
if (schema.properties.hasOwnProperty(k)) { |
|||
const path = options.path.slice(); |
|||
path.push(k); |
|||
if (options.ignore[ObjectPath.stringify(path)] !== true) { |
|||
const required = schema.required && schema.required.indexOf(k) !== -1; |
|||
|
|||
const def = defaultFormDefinition(k, schema.properties[k], { |
|||
path, |
|||
required: required || false, |
|||
lookup: options.lookup, |
|||
ignore: options.ignore, |
|||
global: options.global |
|||
}); |
|||
if (def) { |
|||
f.items.push(def); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return f; |
|||
} |
|||
}; |
|||
|
|||
const array = (name: string, schema: any, options: DefaultsFormOptions): any => { |
|||
|
|||
if (stripNullType(schema.type) === 'array') { |
|||
const f = stdFormObj(name, schema, options); |
|||
f.type = 'array'; |
|||
f.key = options.path; |
|||
options.lookup[ObjectPath.stringify(options.path)] = f; |
|||
|
|||
// don't do anything if items is not defined.
|
|||
if (typeof schema.items !== 'undefined') { |
|||
const required = schema.required && schema.required.indexOf(options.path[options.path.length - 1]) !== -1; |
|||
|
|||
const arrPath = options.path.slice(); |
|||
arrPath.push(''); |
|||
const def = defaultFormDefinition(name, schema.items, { |
|||
path: arrPath, |
|||
required: required || false, |
|||
lookup: options.lookup, |
|||
ignore: options.ignore, |
|||
global: options.global |
|||
}); |
|||
if (def) { |
|||
f.items = [def]; |
|||
} else { |
|||
// This is the case that item only contains key value pair for rc-select multipel
|
|||
f.items = schema.items; |
|||
} |
|||
} |
|||
return f; |
|||
} |
|||
}; |
|||
|
|||
const defaults: { [key: string]: ((name: string, schema: any, options: DefaultsFormOptions) => any)[] } = { |
|||
string: [select, text], |
|||
object: [fieldset], |
|||
number: [numberType], |
|||
integer: [integer], |
|||
boolean: [checkbox], |
|||
array: [checkboxes, array], |
|||
date: [date] |
|||
}; |
|||
|
|||
function defaultFormDefinition(name: string, schema: any, options: DefaultsFormOptions): any { |
|||
const rules = defaults[stripNullType(schema.type)]; |
|||
if (rules) { |
|||
let def; |
|||
rules.forEach((rule) => { |
|||
def = rule(name, schema, options); |
|||
if (def) { |
|||
|
|||
// Do we have form defaults in the schema under the x-schema-form-attribute?
|
|||
if (def.schema['x-schema-form'] && _.isObject(def.schema['x-schema-form'])) { |
|||
def = _.extend(def, def.schema['x-schema-form']); |
|||
} |
|||
return def; |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
interface DefaultsFormData { |
|||
form: any[]; |
|||
lookup: { [key: string]: any }; |
|||
} |
|||
|
|||
function getDefaults(schema: any, ignore: { [key: string]: boolean }, globalOptions: FormOption): DefaultsFormData { |
|||
const form = []; |
|||
const lookup: { [key: string]: any } = {}; |
|||
ignore = ignore || {}; |
|||
globalOptions = globalOptions || {}; |
|||
if (stripNullType(schema.type) === 'object') { |
|||
for (const k of Object.keys(schema.properties)) { |
|||
if (schema.properties.hasOwnProperty(k)) { |
|||
if (ignore[k] !== true) { |
|||
const required = schema.required && schema.required.indexOf(k) !== -1; |
|||
const def = defaultFormDefinition(k, schema.properties[k], { |
|||
path: [k], // Path to this property in bracket notation.
|
|||
lookup, // Extra map to register with. Optimization for merger.
|
|||
ignore, // The ignore list of paths (sans root level name)
|
|||
required, // Is it required? (v4 json schema style)
|
|||
global: globalOptions // Global options, including form defaults
|
|||
}); |
|||
if (def) { |
|||
form.push(def); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
throw new Error('Not implemented. Only type "object" allowed at root level of schema.'); |
|||
} |
|||
return {form, lookup}; |
|||
} |
|||
|
|||
const postProcessFn = (form: any[]): any[] => { |
|||
return form; |
|||
}; |
|||
|
|||
function merge(schema: any, form: any[], ignore: { [key: string]: boolean }, options: FormOption, isReadonly?: boolean): any[] { |
|||
form = form || ['*']; |
|||
options = options || {}; |
|||
isReadonly = isReadonly || schema.readonly || schema.readOnly; |
|||
const stdForm = getDefaults(schema, ignore, options); |
|||
const idx = form.indexOf('*'); |
|||
if (idx !== -1) { |
|||
form = form.slice(0, idx).concat(stdForm.form).concat(form.slice(idx + 1)); |
|||
} |
|||
const lookup = stdForm.lookup; |
|||
return postProcessFn(form.map((obj) => { |
|||
|
|||
if (typeof obj === 'string') { |
|||
obj = {key: obj}; |
|||
} |
|||
|
|||
if (obj.key) { |
|||
if (typeof obj.key === 'string') { |
|||
obj.key = ObjectPath.parse(obj.key); |
|||
} |
|||
} |
|||
|
|||
if (obj.titleMap) { |
|||
obj.titleMap = canonicalTitleMap(obj.titleMap); |
|||
} |
|||
|
|||
if (obj.itemForm) { |
|||
obj.items = []; |
|||
const str: string = ObjectPath.stringify(obj.key); |
|||
const lookupForm = lookup[str]; |
|||
lookupForm.items.forEach((item) => { |
|||
const o = _.cloneDeep(obj.itemForm); |
|||
o.key = item.key; |
|||
obj.items.push(o); |
|||
}); |
|||
} |
|||
|
|||
if (obj.key) { |
|||
const strid: string = ObjectPath.stringify(obj.key); |
|||
if (lookup[strid]) { |
|||
const schemaDefaults = lookup[strid]; |
|||
for (const k of Object.keys(schemaDefaults)) { |
|||
if (schemaDefaults.hasOwnProperty(k)) { |
|||
if (obj[k] === undefined) { |
|||
obj[k] = schemaDefaults[k]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (isReadonly === true) { |
|||
obj.readonly = true; |
|||
} |
|||
|
|||
if (obj.items && obj.items.length > 0) { |
|||
obj.items = merge(schema, obj.items, ignore, options, obj.readonly); |
|||
} |
|||
|
|||
if (obj.tabs) { |
|||
obj.tabs.forEach((tab) => { |
|||
tab.items = merge(schema, tab.items, ignore, options, obj.readonly); |
|||
}); |
|||
} |
|||
|
|||
if (obj.type === 'checkbox' && _.isUndefined(obj.schema.default)) { |
|||
obj.schema.default = false; |
|||
} |
|||
|
|||
return obj; |
|||
})); |
|||
} |
|||
|
|||
function selectOrSet(projection: string | string[], obj: any, valueToSet?: any): any { |
|||
const numRe = /^\d+$/; |
|||
|
|||
if (!obj) { |
|||
obj = this; |
|||
} |
|||
const parts = typeof projection === 'string' ? ObjectPath.parse(projection) : projection; |
|||
|
|||
if (typeof valueToSet !== 'undefined' && parts.length === 1) { |
|||
obj[parts[0]] = valueToSet; |
|||
return obj; |
|||
} |
|||
|
|||
if (typeof valueToSet !== 'undefined' && typeof obj[parts[0]] === 'undefined') { |
|||
obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {}; |
|||
} |
|||
|
|||
let value = obj[parts[0]]; |
|||
for (let i = 1; i < parts.length; i++) { |
|||
if (parts[i] === '') { |
|||
return undefined; |
|||
} |
|||
if (typeof valueToSet !== 'undefined') { |
|||
if (i === parts.length - 1) { |
|||
value[parts[i]] = valueToSet; |
|||
return valueToSet; |
|||
} else { |
|||
let tmp = value[parts[i]]; |
|||
if (typeof tmp === 'undefined' || tmp === null) { |
|||
tmp = numRe.test(parts[i + 1]) ? [] : {}; |
|||
value[parts[i]] = tmp; |
|||
} |
|||
value = tmp; |
|||
} |
|||
} else if (value) { |
|||
value = value[parts[i]]; |
|||
} |
|||
} |
|||
return value; |
|||
} |
|||
|
|||
function updateValue(projection: string | string[], obj: any, valueToSet: any): boolean { |
|||
const numRe = /^\d+$/; |
|||
|
|||
if (!obj) { |
|||
obj = this; |
|||
} |
|||
|
|||
if (!obj) { |
|||
return false; |
|||
} |
|||
|
|||
const parts: string[] = isString(projection) ? ObjectPath.parse(projection) : projection; |
|||
|
|||
if (parts.length === 1) { |
|||
return setValue(obj, parts[0], valueToSet); |
|||
} |
|||
|
|||
if (isUndefined(obj[parts[0]])) { |
|||
obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {}; |
|||
} |
|||
|
|||
let value = obj[parts[0]]; |
|||
for (let i = 1; i < parts.length; i++) { |
|||
if (parts[i] === '') { |
|||
return false; |
|||
} |
|||
if (i === parts.length - 1) { |
|||
return setValue(value, parts[i], valueToSet); |
|||
} else { |
|||
let tmp = value[parts[i]]; |
|||
if (isUndefined(tmp) || tmp === null) { |
|||
tmp = numRe.test(parts[i + 1]) ? [] : {}; |
|||
value[parts[i]] = tmp; |
|||
} |
|||
value = tmp; |
|||
} |
|||
} |
|||
return value; |
|||
} |
|||
|
|||
|
|||
function setValue(obj: any, key: string, val: any): boolean { |
|||
let changed = false; |
|||
if (obj) { |
|||
if (isUndefined(val)) { |
|||
if (isDefined(obj[key])) { |
|||
delete obj[key]; |
|||
changed = true; |
|||
} |
|||
} else { |
|||
changed = !equal(obj[key], val); |
|||
obj[key] = val; |
|||
} |
|||
} |
|||
return changed; |
|||
} |
|||
|
|||
|
|||
const utils = { |
|||
validateBySchema, |
|||
validate, |
|||
merge, |
|||
updateValue, |
|||
selectOrSet |
|||
}; |
|||
export default utils; |
|||
@ -0,0 +1,119 @@ |
|||
///
|
|||
/// 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 { isUndefined, isDefined, isString } from '@app/core/utils'; |
|||
import * as equal from 'deep-equal'; |
|||
import ObjectPath from 'objectpath'; |
|||
import * as React from 'react'; |
|||
|
|||
export interface SchemaValidationResult { |
|||
valid: boolean; |
|||
error?: { |
|||
message?: string; |
|||
} |
|||
} |
|||
|
|||
export interface FormOption { |
|||
formDefaults?: { |
|||
startEmpty?: boolean; |
|||
readonly?: boolean; |
|||
}; |
|||
supressPropertyTitles?: boolean; |
|||
} |
|||
|
|||
export interface DefaultsFormOptions { |
|||
global?: FormOption; |
|||
required?: boolean; |
|||
path?: string[]; |
|||
lookup?: {[key: string]: any}; |
|||
ignore?: {[key: string]: boolean}; |
|||
} |
|||
|
|||
export interface GroupInfo { |
|||
formIndex: number; |
|||
GroupTitle: string; |
|||
} |
|||
|
|||
export interface JsonFormProps { |
|||
model?: any; |
|||
schema?: any; |
|||
form?: any; |
|||
groupInfoes?: GroupInfo[]; |
|||
isFullscreen: boolean; |
|||
ignore?: {[key: string]: boolean}; |
|||
option: FormOption; |
|||
onModelChange?: (key: string | string[], val: any) => void; |
|||
onColorClick?: (event: MouseEvent, key: string, val: string) => void; |
|||
onToggleFullscreen?: () => void; |
|||
mapper?: {[type: string]: any}; |
|||
} |
|||
|
|||
export interface KeyLabelItem { |
|||
key: string; |
|||
label: string; |
|||
value?: string; |
|||
} |
|||
|
|||
export interface JsonFormData { |
|||
type: string; |
|||
key: string | string[]; |
|||
title: string; |
|||
readonly: boolean; |
|||
required: boolean; |
|||
default?: any; |
|||
condition?: string; |
|||
style?: any; |
|||
rows?: number; |
|||
rowsMax?: number; |
|||
placeholder?: string; |
|||
schema: { |
|||
type: string; |
|||
default: any; |
|||
}; |
|||
titleMap: { |
|||
value: any; |
|||
name: string; |
|||
}[]; |
|||
items?: Array<KeyLabelItem>; |
|||
tags?: any; |
|||
[key: string]: any; |
|||
} |
|||
|
|||
export interface JsonFormFieldProps { |
|||
value: any; |
|||
model: any; |
|||
form: JsonFormData; |
|||
builder: (form: JsonFormData, |
|||
model: any, |
|||
index: number, |
|||
onChange: (key: string | string[], val: any) => void, |
|||
onColorClick: (event: MouseEvent, key: string, val: string) => void, |
|||
onToggleFullscreen: () => void, |
|||
mapper: {[type: string]: any}) => JSX.Element; |
|||
mapper?: {[type: string]: any}; |
|||
onChange?: (key: string | string[], val: any) => void; |
|||
onColorClick?: (event: MouseEvent, key: string, val: string) => void; |
|||
onChangeValidate?: (e: any) => void; |
|||
onToggleFullscreen?: () => void; |
|||
valid?: boolean; |
|||
error?: string; |
|||
} |
|||
|
|||
export interface JsonFormFieldState { |
|||
value?: any; |
|||
valid?: boolean; |
|||
error?: string; |
|||
} |
|||
@ -0,0 +1,157 @@ |
|||
/** |
|||
* 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 "~compass-sass-mixins/lib/compass"; |
|||
|
|||
$swift-ease-out-duration: .4s !default; |
|||
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; |
|||
|
|||
$input-label-float-offset: 6px !default; |
|||
$input-label-float-scale: .75 !default; |
|||
|
|||
.tb-json-form { |
|||
&.tb-fullscreen { |
|||
[name="ReactSchemaForm"] { |
|||
.SchemaForm { |
|||
&.SchemaFormFullscreen { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
|
|||
> div:not(.fullscreen-form-field) { |
|||
display: none !important; |
|||
} |
|||
|
|||
> div.fullscreen-form-field { |
|||
position: relative; |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
|
|||
> div { |
|||
> section { |
|||
margin: 0 !important; |
|||
box-shadow: none !important; |
|||
|
|||
.SchemaGroupname { |
|||
display: none !important; |
|||
} |
|||
|
|||
> div { |
|||
padding: 0 !important; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
.json-form-error { |
|||
position: relative; |
|||
bottom: -5px; |
|||
font-size: 12px; |
|||
line-height: 12px; |
|||
color: rgb(244, 67, 54); |
|||
|
|||
transition: all 450ms cubic-bezier(.23, 1, .32, 1) 0ms; |
|||
} |
|||
|
|||
.tb-container { |
|||
position: relative; |
|||
padding: 10px 0; |
|||
margin-top: 32px; |
|||
} |
|||
|
|||
.tb-field { |
|||
padding-bottom: 18px; |
|||
&.tb-required { |
|||
label::after { |
|||
font-size: 13px; |
|||
color: rgba(0, 0, 0, .54); |
|||
vertical-align: top; |
|||
content: " *"; |
|||
} |
|||
} |
|||
|
|||
&.tb-focused:not(.tb-readonly) { |
|||
label::after { |
|||
color: rgb(221, 44, 0); |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-date-field { |
|||
&.tb-required { |
|||
div > div:first-child::after { |
|||
font-size: 13px; |
|||
color: rgba(0, 0, 0, .54); |
|||
vertical-align: top; |
|||
content: " *"; |
|||
} |
|||
} |
|||
|
|||
&.tb-focused:not(.tb-readonly) { |
|||
div > div:first-child::after { |
|||
color: rgb(221, 44, 0); |
|||
} |
|||
} |
|||
} |
|||
|
|||
label.tb-label { |
|||
position: absolute; |
|||
right: auto; |
|||
bottom: 100%; |
|||
left: 0; |
|||
color: rgba(0, 0, 0, .54); |
|||
|
|||
transition: transform $swift-ease-out-timing-function $swift-ease-out-duration, width $swift-ease-out-timing-function $swift-ease-out-duration; |
|||
|
|||
transform: translate3d(0, $input-label-float-offset, 0) scale($input-label-float-scale); |
|||
transform-origin: left top; |
|||
-webkit-font-smoothing: antialiased; |
|||
|
|||
&.tb-focused { |
|||
color: rgb(96, 125, 139); |
|||
} |
|||
|
|||
&.tb-required::after { |
|||
font-size: 13px; |
|||
color: rgba(0, 0, 0, .54); |
|||
vertical-align: top; |
|||
content: " *"; |
|||
} |
|||
|
|||
&.tb-focused:not(.tb-readonly)::after { |
|||
color: rgb(221, 44, 0); |
|||
} |
|||
} |
|||
|
|||
.tb-head-label { |
|||
color: rgba(0, 0, 0, .54); |
|||
} |
|||
|
|||
.SchemaGroupname { |
|||
padding: 10px 20px; |
|||
background-color: #f1f1f1; |
|||
} |
|||
|
|||
.invisible { |
|||
display: none; |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
///
|
|||
/// 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. |
|||
*/ |
|||
import indigo from '@material-ui/core/colors/indigo'; |
|||
import deeepOrange from '@material-ui/core/colors/deepOrange'; |
|||
import { ThemeOptions } from '@material-ui/core/styles/createMuiTheme'; |
|||
import { PaletteOptions } from '@material-ui/core/styles/createPalette'; |
|||
import { deepClone } from '@core/utils'; |
|||
|
|||
const PRIMARY_COLOR = '#305680'; |
|||
const SECONDARY_COLOR = '#527dad'; |
|||
const HUE3_COLOR = '#a7c1de'; |
|||
|
|||
const tbIndigo = deepClone(indigo); |
|||
tbIndigo['500'] = PRIMARY_COLOR; |
|||
tbIndigo['600'] = SECONDARY_COLOR; |
|||
tbIndigo['700'] = tbIndigo['500']; |
|||
tbIndigo.A100 = HUE3_COLOR; |
|||
|
|||
const thingsboardPalette: PaletteOptions = { |
|||
primary: tbIndigo, |
|||
secondary: deeepOrange, |
|||
background: { |
|||
default: '#eee' |
|||
} |
|||
}; |
|||
|
|||
export default { |
|||
typography: { |
|||
fontFamily: 'Roboto, \'Helvetica Neue\', sans-serif' |
|||
}, |
|||
palette: thingsboardPalette, |
|||
} as ThemeOptions; |
|||
@ -0,0 +1,265 @@ |
|||
///
|
|||
/// 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, |
|||
Directive, |
|||
ElementRef, |
|||
OnDestroy, |
|||
Renderer, |
|||
Renderer2, |
|||
HostListener, |
|||
QueryList, |
|||
Output, |
|||
EventEmitter |
|||
} from '@angular/core'; |
|||
import { MatChip, MatChipList } from '@angular/material/chips'; |
|||
import DragStartEvent = JQuery.DragStartEvent; |
|||
import Timeout = NodeJS.Timeout; |
|||
|
|||
export interface MatChipDropEvent { |
|||
from: number; |
|||
to: number; |
|||
} |
|||
|
|||
@Directive({ |
|||
selector: 'mat-chip-list[tb-chip-draggable]', |
|||
}) |
|||
export class MatChipDraggableDirective implements AfterViewInit { |
|||
|
|||
@Output() |
|||
chipDrop = new EventEmitter<MatChipDropEvent>(); |
|||
|
|||
private draggableChips: Array<DraggableChip> = []; |
|||
|
|||
constructor(private chipsList: MatChipList, |
|||
private elementRef: ElementRef<HTMLElement>) { |
|||
} |
|||
|
|||
@HostListener('document:mouseup', ['$event']) |
|||
onDocumentMouseUp() { |
|||
this.draggableChips.forEach((draggableChip) => { |
|||
draggableChip.preventDrag = false; |
|||
}); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
this.configureDraggableChipList(); |
|||
this.chipsList.chips.changes.subscribe(() => { |
|||
this.configureDraggableChipList(); |
|||
}); |
|||
} |
|||
|
|||
private configureDraggableChipList() { |
|||
const toRemove: Array<DraggableChip> = []; |
|||
this.chipsList.chips.forEach((chip) => { |
|||
const found = this.draggableChips.find((draggableChip) => draggableChip.chip === chip); |
|||
if (!found) { |
|||
this.draggableChips.push(new DraggableChip(chip, |
|||
this.chipsList, |
|||
this.elementRef.nativeElement, |
|||
this.chipDrop)); |
|||
} |
|||
} |
|||
); |
|||
this.draggableChips.forEach((draggableChip) => { |
|||
const found = this.chipsList.chips.find((chip) => chip === draggableChip.chip); |
|||
if (!found) { |
|||
toRemove.push(draggableChip); |
|||
} |
|||
}); |
|||
toRemove.forEach((draggableChip) => { |
|||
const index = this.draggableChips.indexOf(draggableChip); |
|||
this.draggableChips.splice(index, 1); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
const draggingClassName = 'dragging'; |
|||
const droppingClassName = 'dropping'; |
|||
const droppingBeforeClassName = 'dropping-before'; |
|||
const droppingAfterClassName = 'dropping-after'; |
|||
|
|||
let globalDraggingChipListId = null; |
|||
|
|||
class DraggableChip { |
|||
|
|||
private chipElement: HTMLElement; |
|||
private handle: HTMLElement; |
|||
|
|||
private dragging = false; |
|||
private counter = 0; |
|||
|
|||
private dropPosition: 'after' | 'before'; |
|||
|
|||
private dropTimeout: Timeout; |
|||
|
|||
public preventDrag = false; |
|||
|
|||
private dropHandler = this.onDrop.bind(this); |
|||
private dragOverHandler = this.onDragOver.bind(this); |
|||
|
|||
constructor(public chip: MatChip, |
|||
private chipsList: MatChipList, |
|||
private chipListElement: HTMLElement, |
|||
private chipDrop: EventEmitter<MatChipDropEvent>) { |
|||
this.chipElement = chip._elementRef.nativeElement; |
|||
this.chipElement.setAttribute('draggable', 'true'); |
|||
this.handle = this.chipElement.getElementsByClassName('tb-chip-drag-handle')[0] as HTMLElement; |
|||
this.chipElement.addEventListener('mousedown', this.onMouseDown.bind(this)); |
|||
this.chipElement.addEventListener('dragstart', this.onDragStart.bind(this)); |
|||
this.chipElement.addEventListener('dragend', this.onDragEnd.bind(this)); |
|||
this.chipElement.addEventListener('dragenter', this.onDragEnter.bind(this)); |
|||
this.chipElement.addEventListener('dragleave', this.onDragLeave.bind(this)); |
|||
} |
|||
|
|||
private onMouseDown(event: MouseEvent) { |
|||
if (event.target !== this.handle) { |
|||
this.preventDrag = true; |
|||
} |
|||
} |
|||
|
|||
private onDragStart(event: DragEvent) { |
|||
if (this.preventDrag) { |
|||
event.preventDefault(); |
|||
} else { |
|||
this.dragging = true; |
|||
globalDraggingChipListId = this.chipListElement.id; |
|||
this.chipListElement.classList.add(draggingClassName); |
|||
this.chipElement.classList.add(draggingClassName); |
|||
event = (event as any).originalEvent || event; |
|||
const dataTransfer = event.dataTransfer; |
|||
dataTransfer.effectAllowed = 'copyMove'; |
|||
dataTransfer.dropEffect = 'move'; |
|||
dataTransfer.setData('text/plain', this.index() + ''); |
|||
} |
|||
} |
|||
|
|||
private onDragEnter(event: DragEvent) { |
|||
this.counter++; |
|||
if (this.dragging) { |
|||
return; |
|||
} |
|||
this.chipElement.removeEventListener('dragover', this.dragOverHandler); |
|||
this.chipElement.removeEventListener('drop', this.dropHandler); |
|||
|
|||
this.chipElement.addEventListener('dragover', this.dragOverHandler); |
|||
this.chipElement.addEventListener('drop', this.dropHandler); |
|||
} |
|||
|
|||
private onDragLeave(event: DragEvent) { |
|||
this.counter--; |
|||
if (this.counter <= 0) { |
|||
this.counter = 0; |
|||
this.chipElement.classList.remove(droppingClassName); |
|||
this.chipElement.classList.remove(droppingAfterClassName); |
|||
this.chipElement.classList.remove(droppingBeforeClassName); |
|||
} |
|||
} |
|||
|
|||
private onDragEnd(event: DragEvent) { |
|||
this.dragging = false; |
|||
globalDraggingChipListId = null; |
|||
this.chipListElement.classList.remove(draggingClassName); |
|||
this.chipElement.classList.remove(draggingClassName); |
|||
} |
|||
|
|||
private onDragOver(event: DragEvent) { |
|||
if (this.dragging) { |
|||
return; |
|||
} |
|||
event.preventDefault(); |
|||
if (globalDraggingChipListId !== this.chipListElement.id) { |
|||
return; |
|||
} |
|||
const bounds = this.chipElement.getBoundingClientRect(); |
|||
event = (event as any).originalEvent || event; |
|||
const props = { |
|||
width: bounds.right - bounds.left, |
|||
height: bounds.bottom - bounds.top, |
|||
x: event.clientX - bounds.left, |
|||
y: event.clientY - bounds.top, |
|||
}; |
|||
|
|||
const horizontalOffset = props.x; |
|||
const horizontalMidPoint = props.width / 2; |
|||
|
|||
const verticalOffset = props.y; |
|||
const verticalMidPoint = props.height / 2; |
|||
|
|||
this.chipElement.classList.add(droppingClassName); |
|||
|
|||
this.chipElement.classList.remove(droppingAfterClassName); |
|||
this.chipElement.classList.remove(droppingBeforeClassName); |
|||
|
|||
if (horizontalOffset >= horizontalMidPoint || verticalOffset >= verticalMidPoint) { |
|||
this.dropPosition = 'after'; |
|||
this.chipElement.classList.add(droppingAfterClassName); |
|||
} else { |
|||
this.dropPosition = 'before'; |
|||
this.chipElement.classList.add(droppingBeforeClassName); |
|||
} |
|||
|
|||
} |
|||
|
|||
private onDrop(event: DragEvent) { |
|||
this.counter = 0; |
|||
event.preventDefault(); |
|||
if (globalDraggingChipListId !== this.chipListElement.id) { |
|||
return; |
|||
} |
|||
event = (event as any).originalEvent || event; |
|||
const droppedItemIndex = parseInt(event.dataTransfer.getData('text/plain'), 10); |
|||
const currentIndex = this.index(); |
|||
let newIndex; |
|||
if (this.dropPosition === 'before') { |
|||
if (droppedItemIndex < currentIndex) { |
|||
newIndex = currentIndex - 1; |
|||
} else { |
|||
newIndex = currentIndex; |
|||
} |
|||
} else { |
|||
if (droppedItemIndex < currentIndex) { |
|||
newIndex = currentIndex; |
|||
} else { |
|||
newIndex = currentIndex + 1; |
|||
} |
|||
} |
|||
if (this.dropTimeout) { |
|||
clearTimeout(this.dropTimeout); |
|||
} |
|||
this.dropTimeout = setTimeout(() => { |
|||
this.dropPosition = null; |
|||
|
|||
this.chipElement.classList.remove(droppingClassName); |
|||
this.chipElement.classList.remove(droppingAfterClassName); |
|||
this.chipElement.classList.remove(droppingBeforeClassName); |
|||
|
|||
this.chipElement.removeEventListener('drop', this.dropHandler); |
|||
|
|||
const dropEvent: MatChipDropEvent = { |
|||
from: droppedItemIndex, |
|||
to: newIndex |
|||
}; |
|||
this.chipDrop.emit(dropEvent); |
|||
}, 1000 / 16); |
|||
} |
|||
|
|||
private index(): number { |
|||
return this.chipsList.chips.toArray().indexOf(this.chip); |
|||
} |
|||
|
|||
} |
|||
@ -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. |
|||
*/ |
|||
|
|||
@mixin tb-mat-icon-size($size) { |
|||
width: #{$size}px; |
|||
min-width: #{$size}px; |
|||
height: #{$size}px; |
|||
min-height: #{$size}px; |
|||
font-size: #{$size}px; |
|||
line-height: #{$size}px; |
|||
svg { |
|||
width: 24px; |
|||
height: 24px; |
|||
transform: scale($size/24); |
|||
transform-origin: top; |
|||
} |
|||
} |
|||
|
|||
@mixin tb-mat-icon-button-size($size) { |
|||
width: #{$size}px; |
|||
height: #{$size}px; |
|||
line-height: #{$size}px; |
|||
padding: 0 !important; |
|||
.mat-icon { |
|||
display: block; |
|||
margin: auto; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue