committed by
GitHub
122 changed files with 4880 additions and 5990 deletions
@ -0,0 +1,74 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-dynamic-form-array tb-form-panel stroked" [class.no-padding-right]="!disabled || !propertiesFormArray().controls.length"> |
||||
|
<mat-expansion-panel class="tb-settings" expanded> |
||||
|
<mat-expansion-panel-header class="flex flex-row flex-wrap"> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-form-panel-title">{{ title }}</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div *ngIf="propertiesFormArray().controls.length; else noProperties" class="tb-dynamic-form-array-body tb-drop-list" |
||||
|
cdkDropList cdkDropListOrientation="vertical" |
||||
|
[cdkDropListDisabled]="!dragEnabled" |
||||
|
(cdkDropListDropped)="propertyDrop($event)"> |
||||
|
<div cdkDrag [cdkDragDisabled]="!dragEnabled" |
||||
|
class="tb-draggable tb-dynamic-form-array-row" |
||||
|
*ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;"> |
||||
|
<div class="tb-dynamic-form-item-index-container"> |
||||
|
<div class="tb-dynamic-form-item-index">{{$index + 1}}</div> |
||||
|
</div> |
||||
|
<tb-dynamic-form class="flex-1" |
||||
|
[properties]="[itemProperty]" |
||||
|
[formControl]="propertyControl" |
||||
|
isArrayItem noPadding noBorder> |
||||
|
</tb-dynamic-form> |
||||
|
<div class="tb-dynamic-form-array-buttons"> |
||||
|
<button mat-icon-button |
||||
|
*ngIf="!disabled" |
||||
|
type="button" |
||||
|
(click)="removeProperty($index)" |
||||
|
matTooltip="{{ 'action.delete' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
*ngIf="!disabled" |
||||
|
type="button" |
||||
|
cdkDragHandle |
||||
|
[class.tb-hidden]="!dragEnabled" |
||||
|
matTooltip="{{ 'action.drag' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>drag_indicator</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="!disabled || !propertiesFormArray().controls.length" [class.invisible]="disabled"> |
||||
|
<button type="button" mat-stroked-button color="primary" (click)="addProperty()"> |
||||
|
<mat-icon>add</mat-icon> |
||||
|
{{ 'action.add' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
|
||||
|
<ng-template #noProperties> |
||||
|
<span class="tb-prompt flex items-center justify-center">{{ 'dynamic-form.property.no-items' | translate }}</span> |
||||
|
</ng-template> |
||||
@ -0,0 +1,72 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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 "../../../../../../../../../scss/constants"; |
||||
|
|
||||
|
.tb-dynamic-form-array { |
||||
|
.tb-dynamic-form-array-body { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
} |
||||
|
.tb-dynamic-form-array-row { |
||||
|
background: #fff; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
place-content: center flex-start; |
||||
|
align-items: flex-start; |
||||
|
.tb-dynamic-form-item-index-container { |
||||
|
width: 36px; |
||||
|
height: 56px; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
place-content: flex-start; |
||||
|
align-items: center; |
||||
|
.tb-dynamic-form-item-index { |
||||
|
width: 24px; |
||||
|
height: 24px; |
||||
|
position: relative; |
||||
|
font-weight: 400; |
||||
|
font-size: 16px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
place-content: center; |
||||
|
color: $tb-primary-color; |
||||
|
&:before { |
||||
|
content: ""; |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
position: absolute; |
||||
|
left: 0; |
||||
|
top: 0; |
||||
|
background-color: $tb-primary-color; |
||||
|
opacity: 0.06; |
||||
|
border-radius: 100%; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-dynamic-form-array-buttons { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
button.mat-mdc-icon-button.mat-mdc-button-base { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
&.tb-hidden { |
||||
|
visibility: hidden; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,161 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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, DestroyRef, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
Validator |
||||
|
} from '@angular/forms'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { defaultFormPropertyValue, FormProperty } from '@shared/models/dynamic-form.models'; |
||||
|
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-dynamic-form-array', |
||||
|
templateUrl: './dynamic-form-array.component.html', |
||||
|
styleUrls: ['./dynamic-form-array.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => DynamicFormArrayComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => DynamicFormArrayComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class DynamicFormArrayComponent implements ControlValueAccessor, OnInit, Validator { |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
itemProperty: FormProperty; |
||||
|
|
||||
|
@Input() |
||||
|
title: string; |
||||
|
|
||||
|
propertiesFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
get dragEnabled(): boolean { |
||||
|
return !this.disabled && this.propertiesFormArray().controls.length > 1; |
||||
|
} |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private destroyRef: DestroyRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.propertiesFormGroup = this.fb.group({ |
||||
|
properties: this.fb.array([]) |
||||
|
}); |
||||
|
this.propertiesFormGroup.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe( |
||||
|
() => { |
||||
|
const properties: {[id: string]: any}[] = this.propertiesFormGroup.get('properties').value; |
||||
|
const value = properties.map(prop => prop[this.itemProperty.id]); |
||||
|
this.propagateChange(value); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.propertiesFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertiesFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(values: any[] | undefined): void { |
||||
|
this.propertiesFormGroup.setControl('properties', this.preparePropertiesFormArray(values || []), {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
public validate(_c: UntypedFormControl) { |
||||
|
const valid = this.propertiesFormGroup.valid; |
||||
|
return valid ? null : { |
||||
|
properties: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
propertyDrop(event: CdkDragDrop<string[]>) { |
||||
|
const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; |
||||
|
const property = propertiesArray.at(event.previousIndex); |
||||
|
propertiesArray.removeAt(event.previousIndex, {emitEvent: false}); |
||||
|
propertiesArray.insert(event.currentIndex, property, {emitEvent: true}); |
||||
|
} |
||||
|
|
||||
|
propertiesFormArray(): UntypedFormArray { |
||||
|
return this.propertiesFormGroup.get('properties') as UntypedFormArray; |
||||
|
} |
||||
|
|
||||
|
trackByProperty(_index: number, propertyControl: AbstractControl): any { |
||||
|
return propertyControl; |
||||
|
} |
||||
|
|
||||
|
removeProperty(index: number, emitEvent = true) { |
||||
|
(this.propertiesFormGroup.get('properties') as UntypedFormArray).removeAt(index, {emitEvent}); |
||||
|
} |
||||
|
|
||||
|
addProperty() { |
||||
|
const property = { |
||||
|
[this.itemProperty.id]: defaultFormPropertyValue(this.itemProperty) |
||||
|
}; |
||||
|
const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; |
||||
|
const propertyControl = this.fb.control(property, []); |
||||
|
propertiesArray.push(propertyControl); |
||||
|
setTimeout(() => { |
||||
|
propertyControl.updateValueAndValidity(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private preparePropertiesFormArray(values: any[] | undefined): UntypedFormArray { |
||||
|
const propertiesControls: Array<AbstractControl> = []; |
||||
|
if (values) { |
||||
|
values.forEach((value) => { |
||||
|
const property = { |
||||
|
[this.itemProperty.id]: value |
||||
|
}; |
||||
|
propertiesControls.push(this.fb.control(property, [])); |
||||
|
}); |
||||
|
} |
||||
|
return this.fb.array(propertiesControls); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-form-panel tb-dynamic-form-properties" [class.no-border]="noBorder" [class.no-padding]="noBorder" [class.no-margin]="noMargin"> |
||||
|
<div class="tb-form-table" [class.flex-1]="fillHeight"> |
||||
|
<div class="tb-form-table-header" [class.no-padding-right]="!disabled"> |
||||
|
<div class="tb-form-table-header-cell tb-id-header" translate>dynamic-form.property.id</div> |
||||
|
<div class="tb-form-table-header-cell tb-name-header" translate>dynamic-form.property.name</div> |
||||
|
<div class="tb-form-table-header-cell tb-type-header" translate>dynamic-form.property.type</div> |
||||
|
<div class="tb-form-table-header-cell tb-actions-header tb-form-table-row-cell-buttons justify-end" [class.disabled]="disabled"> |
||||
|
<button mat-icon-button |
||||
|
*ngIf="!disabled && importExport" |
||||
|
type="button" |
||||
|
matTooltip="{{ 'dynamic-form.import-form' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="import($event)"> |
||||
|
<tb-icon>mdi:file-import</tb-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
*ngIf="!disabled && importExport" |
||||
|
[class.tb-hidden]="!propertiesFormArray().controls.length" |
||||
|
type="button" |
||||
|
[disabled]="!propertiesFormArray().controls.length" |
||||
|
matTooltip="{{ 'dynamic-form.export-form' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="export($event)"> |
||||
|
<tb-icon>mdi:file-export</tb-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button |
||||
|
*ngIf="!disabled" |
||||
|
[class.tb-hidden]="!propertiesFormArray().controls.length" |
||||
|
type="button" |
||||
|
[disabled]="!propertiesFormArray().controls.length" |
||||
|
matTooltip="{{ 'dynamic-form.clear-form' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
(click)="clear($event)"> |
||||
|
<tb-icon>mdi:broom</tb-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="propertiesFormArray().controls.length; else noProperties" class="tb-form-table-body tb-drop-list" |
||||
|
cdkDropList cdkDropListOrientation="vertical" |
||||
|
[cdkDropListDisabled]="!dragEnabled" |
||||
|
(cdkDropListDropped)="propertyDrop($event)"> |
||||
|
<div cdkDrag [cdkDragDisabled]="!dragEnabled" |
||||
|
[class.tb-draggable]="!disabled" |
||||
|
[class.tb-draggable-form-table-row]="!disabled" |
||||
|
*ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;"> |
||||
|
<tb-dynamic-form-property-row class="flex-1" |
||||
|
[index]="$index" |
||||
|
[booleanPropertyIds]="booleanPropertyIds" |
||||
|
[formControl]="propertyControl" |
||||
|
(propertyRemoved)="removeProperty($index)"> |
||||
|
</tb-dynamic-form-property-row> |
||||
|
<div class="tb-form-table-row-cell-buttons"> |
||||
|
<button mat-icon-button |
||||
|
*ngIf="!disabled" |
||||
|
type="button" |
||||
|
cdkDragHandle |
||||
|
[class.tb-hidden]="!dragEnabled" |
||||
|
matTooltip="{{ 'action.drag' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>drag_indicator</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-error *ngIf="errorText" noMargin [error]="errorText" style="padding-left: 12px;"></tb-error> |
||||
|
</div> |
||||
|
<div *ngIf="!disabled"> |
||||
|
<button type="button" mat-stroked-button color="primary" (click)="addProperty()"> |
||||
|
{{ 'dynamic-form.property.add-property' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<ng-template #noProperties> |
||||
|
<span class="tb-prompt flex items-center justify-center" [style]="fillHeight ? {height: '100%'} : {}">{{ 'dynamic-form.property.no-properties' | translate }}</span> |
||||
|
</ng-template> |
||||
@ -0,0 +1,471 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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-dynamic-form-property-settings-panel" [formGroup]="propertyFormGroup"> |
||||
|
<div class="tb-dynamic-form-property-settings-title">{{ panelTitle | translate }}</div> |
||||
|
<div class="tb-dynamic-form-property-settings-panel-content"> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.id</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.name</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.group-title</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="group" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.type</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="type"> |
||||
|
<mat-option *ngFor="let type of formPropertyTypes" [value]="type"> |
||||
|
{{ formPropertyTypeTranslations.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div *ngIf="isArray" class="tb-form-panel stroked"> |
||||
|
<div class="tb-form-panel-title" translate>dynamic-form.property.array-item</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.item-type</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="arrayItemType"> |
||||
|
<mat-option *ngFor="let type of arrayItemFormPropertyTypes" [value]="type"> |
||||
|
{{ formPropertyTypeTranslations.get(type) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.item-name</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="arrayItemName" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<ng-container *ngTemplateOutlet="itemSettings"> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
<ng-container *ngIf="!isArray"> |
||||
|
<ng-container *ngTemplateOutlet="itemSettings"> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
<div *ngIf="![FormPropertyType.fieldset, FormPropertyType.htmlSection, FormPropertyType.array].includes(propertyFormGroup.get('type').value)" class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="required"> |
||||
|
{{ 'dynamic-form.property.value-required' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<mat-expansion-panel class="tb-settings"> |
||||
|
<mat-expansion-panel-header class="flex flex-row flex-wrap"> |
||||
|
<mat-panel-title> |
||||
|
{{ 'dynamic-form.property.advanced-ui-settings' | translate }} |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<ng-container *ngIf="isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value)"> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.sub-label</div> |
||||
|
<mat-form-field class="flex" |
||||
|
appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="subLabel" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="divider"> |
||||
|
{{ 'dynamic-form.property.vertical-divider-after' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
<div *ngIf="[FormPropertyType.number, FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)" |
||||
|
class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.input-field-suffix</div> |
||||
|
<mat-form-field class="flex" |
||||
|
appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="fieldSuffix" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.htmlSection" class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.disable-on-property</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="disableOnProperty" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<mat-option [value]="null"></mat-option> |
||||
|
<mat-option *ngFor="let prop of booleanPropertyIds" [value]="prop"> |
||||
|
{{ prop }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div class="tb-form-row"> |
||||
|
<tb-js-func |
||||
|
class="flex-1" |
||||
|
formControlName="condition" |
||||
|
[functionArgs]="['property', 'model']" |
||||
|
functionTitle="{{ 'dynamic-form.property.display-condition-function' | translate }}"> |
||||
|
</tb-js-func> |
||||
|
</div> |
||||
|
<tb-string-items-list *ngIf="isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value)" |
||||
|
class="flex-1" |
||||
|
fieldClass="flex" |
||||
|
floatLabel="always" |
||||
|
label="{{ 'dynamic-form.property.property-row-classes' | translate }}" |
||||
|
placeholder="{{ 'widget-config.set' | translate }}" |
||||
|
[predefinedValues]="formPropertyRowClasses" |
||||
|
formControlName="rowClass" |
||||
|
appearance="outline" |
||||
|
subscriptSizing="dynamic"> |
||||
|
</tb-string-items-list> |
||||
|
<tb-string-items-list *ngIf="isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value) && FormPropertyType.switch !== propertyFormGroup.get('type').value" |
||||
|
class="flex-1" |
||||
|
fieldClass="flex" |
||||
|
floatLabel="always" |
||||
|
label="{{ 'dynamic-form.property.property-field-classes' | translate }}" |
||||
|
placeholder="{{ 'widget-config.set' | translate }}" |
||||
|
[predefinedValues]="formPropertyFieldClasses" |
||||
|
formControlName="fieldClass" |
||||
|
appearance="outline" |
||||
|
subscriptSizing="dynamic"> |
||||
|
</tb-string-items-list> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-dynamic-form-property-settings-panel-buttons"> |
||||
|
<button mat-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="cancel()"> |
||||
|
{{ (disabled ? 'action.close' : 'action.cancel') | translate }} |
||||
|
</button> |
||||
|
<button *ngIf="!disabled" |
||||
|
mat-raised-button |
||||
|
color="primary" |
||||
|
type="button" |
||||
|
(click)="applyPropertySettings()" |
||||
|
[disabled]="propertyFormGroup.invalid || !propertyFormGroup.dirty"> |
||||
|
{{ (isAdd ? 'action.add' : 'action.apply') | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<ng-template #itemSettings> |
||||
|
<ng-container [formGroup]="propertyFormGroup"> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.textarea" class="tb-form-row space-between"> |
||||
|
<div translate>dynamic-form.property.textarea-rows</div> |
||||
|
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="rows" min="1" step="1" |
||||
|
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.number" class="tb-form-row space-between"> |
||||
|
<div translate>dynamic-form.property.number-settings</div> |
||||
|
<div class="flex flex-1 flex-row items-center justify-end gap-2"> |
||||
|
<div class="tb-small-label" translate>dynamic-form.property.min</div> |
||||
|
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="min" |
||||
|
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<mat-divider vertical></mat-divider> |
||||
|
<div class="tb-small-label" translate>dynamic-form.property.max</div> |
||||
|
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="max" |
||||
|
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<mat-divider vertical></mat-divider> |
||||
|
<div class="tb-small-label" translate>dynamic-form.property.step</div> |
||||
|
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="step" min="0" |
||||
|
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.fieldset" class="tb-form-panel stroked"> |
||||
|
<mat-expansion-panel class="tb-settings" expanded> |
||||
|
<mat-expansion-panel-header class="flex flex-row flex-wrap"> |
||||
|
<mat-panel-title> |
||||
|
{{ 'dynamic-form.property.properties' | translate }} |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<tb-dynamic-form-properties formControlName="properties" noBorder noMargin> |
||||
|
</tb-dynamic-form-properties> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.htmlSection" class="tb-form-panel stroked"> |
||||
|
<mat-expansion-panel class="tb-settings" expanded> |
||||
|
<mat-expansion-panel-header class="flex flex-row flex-wrap"> |
||||
|
<mat-panel-title> |
||||
|
{{ 'dynamic-form.property.html-section-settings' | translate }} |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div class="tb-form-row"> |
||||
|
<tb-html |
||||
|
class="flex-1" |
||||
|
formControlName="htmlContent" |
||||
|
label="{{ 'dynamic-form.property.html-section-content' | translate }}"> |
||||
|
</tb-html> |
||||
|
</div> |
||||
|
<tb-string-items-list class="flex-1" |
||||
|
fieldClass="flex" |
||||
|
floatLabel="always" |
||||
|
label="{{ 'dynamic-form.property.html-section-classes' | translate }}" |
||||
|
formControlName="htmlClassList" |
||||
|
appearance="outline" |
||||
|
subscriptSizing="dynamic"> |
||||
|
</tb-string-items-list> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
<ng-container *ngIf="[FormPropertyType.select, FormPropertyType.radios].includes(propertyItemType)"> |
||||
|
<div class="tb-form-panel stroked"> |
||||
|
<mat-expansion-panel class="tb-settings" expanded> |
||||
|
<mat-expansion-panel-header class="flex flex-row flex-wrap"> |
||||
|
<mat-panel-title> |
||||
|
{{ (propertyFormGroup.get('type').value === FormPropertyType.select ? 'dynamic-form.property.select-options' : 'dynamic-form.property.radio-button-options') | translate }} |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.radios" class="tb-form-row space-between"> |
||||
|
<div translate>dynamic-form.property.buttons-direction</div> |
||||
|
<mat-chip-listbox class="center-stretch" formControlName="direction"> |
||||
|
<mat-chip-option [selectable]="propertyFormGroup.get('direction').value !== 'column'" |
||||
|
value="column">{{ 'dynamic-form.property.direction-column' | translate }}</mat-chip-option> |
||||
|
<mat-chip-option [selectable]="propertyFormGroup.get('direction').value !== 'row'" |
||||
|
value="row">{{ 'dynamic-form.property.direction-row' | translate }}</mat-chip-option> |
||||
|
</mat-chip-listbox> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.select" class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="multiple"> |
||||
|
{{ 'dynamic-form.property.enable-multiple-select' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.select && !propertyFormGroup.get('multiple').value" class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="allowEmptyOption"> |
||||
|
{{ 'dynamic-form.property.allow-empty-select-option' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.select && propertyFormGroup.get('multiple').value" class="tb-form-row space-between"> |
||||
|
<div translate>dynamic-form.property.selected-options-limit</div> |
||||
|
<div class="flex flex-1 flex-row items-center justify-end gap-2"> |
||||
|
<div class="tb-small-label" translate>dynamic-form.property.min</div> |
||||
|
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="minItems" |
||||
|
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<mat-divider vertical></mat-divider> |
||||
|
<div class="tb-small-label" translate>dynamic-form.property.max</div> |
||||
|
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="maxItems" |
||||
|
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<tb-dynamic-form-select-items |
||||
|
formControlName="items"> |
||||
|
</tb-dynamic-form-select-items> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
<div *ngIf="[FormPropertyType.javascript, FormPropertyType.markdown].includes(propertyItemType)" class="tb-form-row"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.help-id</div> |
||||
|
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="helpId" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<ng-container *ngIf="propertyItemType === FormPropertyType.datetime"> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<div translate>dynamic-form.property.datetime-type</div> |
||||
|
<mat-chip-listbox class="center-stretch" formControlName="dateTimeType"> |
||||
|
<mat-chip-option [selectable]="propertyFormGroup.get('dateTimeType').value !== 'date'" |
||||
|
value="date">{{ 'dynamic-form.property.datetime-type-date' | translate }}</mat-chip-option> |
||||
|
<mat-chip-option [selectable]="propertyFormGroup.get('dateTimeType').value !== 'time'" |
||||
|
value="time">{{ 'dynamic-form.property.datetime-type-time' | translate }}</mat-chip-option> |
||||
|
<mat-chip-option [selectable]="propertyFormGroup.get('dateTimeType').value !== 'datetime'" |
||||
|
value="datetime">{{ 'dynamic-form.property.datetime-type-datetime' | translate }}</mat-chip-option> |
||||
|
</mat-chip-listbox> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between"> |
||||
|
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="allowClear"> |
||||
|
{{ 'dynamic-form.property.enable-clear-button' | translate }} |
||||
|
</mat-slide-toggle> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.textarea" class="tb-form-row no-border no-padding"> |
||||
|
<mat-form-field class="flex" |
||||
|
floatLabel="always" |
||||
|
appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label translate>dynamic-form.property.default-value</mat-label> |
||||
|
<textarea matInput |
||||
|
[rows]="propertyFormGroup.get('rows').value || 2" |
||||
|
formControlName="default" placeholder="{{ 'widget-config.set' | translate }}"></textarea> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.javascript" class="tb-form-row" > |
||||
|
<tb-js-func |
||||
|
class="flex-1" |
||||
|
disableUndefinedCheck |
||||
|
formControlName="default" |
||||
|
[helpId]="propertyFormGroup.get('helpId').value" |
||||
|
label="{{ 'dynamic-form.property.default-value' | translate }}"> |
||||
|
</tb-js-func> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.json" class="tb-form-row" > |
||||
|
<tb-json-content |
||||
|
class="flex-1" |
||||
|
formControlName="default" |
||||
|
[contentType]="ContentType.JSON" |
||||
|
label="{{ 'dynamic-form.property.default-value' | translate }}"> |
||||
|
</tb-json-content> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.html" class="tb-form-row" > |
||||
|
<tb-html |
||||
|
class="flex-1" |
||||
|
formControlName="default" |
||||
|
label="{{ 'dynamic-form.property.default-value' | translate }}"> |
||||
|
</tb-html> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.css" class="tb-form-row" > |
||||
|
<tb-css |
||||
|
class="flex-1" |
||||
|
formControlName="default" |
||||
|
label="{{ 'dynamic-form.property.default-value' | translate }}"> |
||||
|
</tb-css> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.markdown" class="tb-form-row" > |
||||
|
<tb-markdown-editor |
||||
|
class="flex-1" |
||||
|
formControlName="default" |
||||
|
[helpId]="propertyFormGroup.get('helpId').value" |
||||
|
label="{{ 'dynamic-form.property.default-value' | translate }}"> |
||||
|
</tb-markdown-editor> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.image && !isArray" class="tb-form-row" > |
||||
|
<tb-gallery-image-input |
||||
|
class="flex-1" |
||||
|
formControlName="default" |
||||
|
label="{{ 'dynamic-form.property.default-value' | translate }}"> |
||||
|
</tb-gallery-image-input> |
||||
|
</div> |
||||
|
<div *ngIf="propertyItemType === FormPropertyType.radios" |
||||
|
class="tb-form-row tb-radios-property" [class.direction-column]="propertyFormGroup.get('direction').value === 'column'"> |
||||
|
<div tbTruncateWithTooltip translate class="fixed-title-height">dynamic-form.property.default-value</div> |
||||
|
<mat-radio-group class="flex flex-1 flex-wrap" [class.flex-col]="propertyFormGroup.get('direction').value === 'column'" |
||||
|
formControlName="default"> |
||||
|
<mat-radio-button *ngFor="let item of propertyFormGroup.get('items').value" [value]="item.value"> |
||||
|
<div tbTruncateWithTooltip>{{ item.label | customTranslate }}</div> |
||||
|
</mat-radio-button> |
||||
|
</mat-radio-group> |
||||
|
</div> |
||||
|
<div *ngIf="isPropertyTypeAllowedForRow(propertyItemType)" class="tb-form-row space-between"> |
||||
|
<div class="fixed-title-width" translate>dynamic-form.property.default-value</div> |
||||
|
<mat-form-field *ngIf="[FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)" |
||||
|
class="flex" |
||||
|
appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput [type]="propertyFormGroup.get('type').value" formControlName="default" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-color-input *ngIf="propertyItemType === FormPropertyType.color" |
||||
|
asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="default"> |
||||
|
</tb-color-input> |
||||
|
<tb-color-settings *ngIf="propertyItemType === FormPropertyType.color_settings" |
||||
|
formControlName="default" |
||||
|
settingsKey="{{ propertyFormGroup.get('id').value }}"> |
||||
|
</tb-color-settings> |
||||
|
<mat-form-field *ngIf="propertyItemType === FormPropertyType.number" |
||||
|
appearance="outline" |
||||
|
class="number" |
||||
|
subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="default" |
||||
|
[min]="propertyFormGroup.get('min').value" |
||||
|
[max]="propertyFormGroup.get('max').value" |
||||
|
[step]="propertyFormGroup.get('step').value" |
||||
|
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<tb-font-settings *ngIf="propertyItemType === FormPropertyType.font" |
||||
|
formControlName="default" |
||||
|
clearButton |
||||
|
disabledLineHeight> |
||||
|
</tb-font-settings> |
||||
|
<tb-unit-input *ngIf="propertyItemType === FormPropertyType.units" |
||||
|
formControlName="default"></tb-unit-input> |
||||
|
<tb-value-input *ngIf="propertyItemType === FormPropertyType.switch" |
||||
|
[valueType]="ValueType.BOOLEAN" |
||||
|
formControlName="default"></tb-value-input> |
||||
|
<tb-material-icon-select *ngIf="propertyItemType === FormPropertyType.icon" |
||||
|
asBoxInput |
||||
|
formControlName="default"> |
||||
|
</tb-material-icon-select> |
||||
|
<mat-form-field *ngIf="propertyItemType === FormPropertyType.select" class="medium-width" |
||||
|
appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select *ngIf="!propertyFormGroup.get('multiple').value" formControlName="default"> |
||||
|
<mat-option *ngIf="propertyFormGroup.get('allowEmptyOption').value" [value]="null"></mat-option> |
||||
|
<mat-option *ngFor="let item of propertyFormGroup.get('items').value" [value]="item.value"> |
||||
|
{{ item.label | customTranslate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-select *ngIf="propertyFormGroup.get('multiple').value" formControlName="default" multiple> |
||||
|
<mat-option *ngFor="let item of propertyFormGroup.get('items').value" [value]="item.value"> |
||||
|
{{ item.label | customTranslate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<ng-container *ngIf="propertyItemType === FormPropertyType.datetime"> |
||||
|
<tb-datetime *ngIf="propertyFormGroup.get('dateTimeType').value === 'date'" |
||||
|
class="flex-1" |
||||
|
showLabel="false" |
||||
|
fieldClass="flex" |
||||
|
[allowClear]="propertyFormGroup.get('allowClear').value" |
||||
|
appearance="outline" |
||||
|
subscriptSizing="dynamic" |
||||
|
type="date" |
||||
|
formControlName="default" |
||||
|
></tb-datetime> |
||||
|
<tb-datetime *ngIf="propertyFormGroup.get('dateTimeType').value === 'time'" |
||||
|
class="flex-1" |
||||
|
showLabel="false" |
||||
|
fieldClass="flex" |
||||
|
[allowClear]="propertyFormGroup.get('allowClear').value" |
||||
|
appearance="outline" |
||||
|
subscriptSizing="dynamic" |
||||
|
type="time" |
||||
|
formControlName="default" |
||||
|
></tb-datetime> |
||||
|
<tb-datetime *ngIf="propertyFormGroup.get('dateTimeType').value === 'datetime'" |
||||
|
class="flex-1" |
||||
|
showLabel="false" |
||||
|
fieldClass="flex" |
||||
|
[allowClear]="propertyFormGroup.get('allowClear').value" |
||||
|
appearance="outline" |
||||
|
subscriptSizing="dynamic" |
||||
|
type="datetime" |
||||
|
formControlName="default" |
||||
|
></tb-datetime> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
@ -0,0 +1,317 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; |
||||
|
import { TbPopoverComponent } from '@shared/components/popover.component'; |
||||
|
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
||||
|
import { ContentType, ValueType } from '@shared/models/constants'; |
||||
|
import { |
||||
|
defaultPropertyValue, |
||||
|
FormProperty, |
||||
|
formPropertyFieldClasses, |
||||
|
formPropertyRowClasses, |
||||
|
FormPropertyType, |
||||
|
formPropertyTypes, |
||||
|
formPropertyTypeTranslations, |
||||
|
FormSelectItem, |
||||
|
isPropertyTypeAllowedForRow |
||||
|
} from '@shared/models/dynamic-form.models'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { isUndefinedOrNull } from '@core/utils'; |
||||
|
import { StringItemsOption } from '@shared/components/string-items-list.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-dynamic-form-property-panel', |
||||
|
templateUrl: './dynamic-form-property-panel.component.html', |
||||
|
styleUrls: ['./dynamic-form-property-panel.component.scss'], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class DynamicFormPropertyPanelComponent implements OnInit { |
||||
|
|
||||
|
ValueType = ValueType; |
||||
|
|
||||
|
FormPropertyType = FormPropertyType; |
||||
|
|
||||
|
ContentType = ContentType; |
||||
|
|
||||
|
formPropertyTypes = formPropertyTypes; |
||||
|
arrayItemFormPropertyTypes = formPropertyTypes.filter(t => t !== FormPropertyType.array); |
||||
|
formPropertyTypeTranslations = formPropertyTypeTranslations; |
||||
|
|
||||
|
formPropertyRowClasses: StringItemsOption[] = formPropertyRowClasses.map(clazz => ({name: clazz, value: clazz})); |
||||
|
|
||||
|
formPropertyFieldClasses: StringItemsOption[] = formPropertyFieldClasses.map(clazz => ({name: clazz, value: clazz})); |
||||
|
|
||||
|
isPropertyTypeAllowedForRow = isPropertyTypeAllowedForRow; |
||||
|
|
||||
|
get propertyItemType(): FormPropertyType | any { |
||||
|
if (this.isArray) { |
||||
|
return this.propertyFormGroup.get('arrayItemType').value; |
||||
|
} else { |
||||
|
return this.propertyFormGroup.get('type').value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
get isArray(): boolean { |
||||
|
return this.propertyFormGroup.get('type').value === FormPropertyType.array; |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
isAdd = false; |
||||
|
|
||||
|
@Input() |
||||
|
property: FormProperty; |
||||
|
|
||||
|
@Input() |
||||
|
booleanPropertyIds: string[]; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
popover: TbPopoverComponent<DynamicFormPropertyPanelComponent>; |
||||
|
|
||||
|
@Output() |
||||
|
propertySettingsApplied = new EventEmitter<FormProperty>(); |
||||
|
|
||||
|
panelTitle: string; |
||||
|
|
||||
|
propertyFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private propertyType: FormPropertyType; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private destroyRef: DestroyRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.panelTitle = this.isAdd ? 'dynamic-form.property.add-property' : 'dynamic-form.property.property-settings'; |
||||
|
this.propertyType = this.property.type === FormPropertyType.array ? this.property.arrayItemType : this.property.type; |
||||
|
this.propertyFormGroup = this.fb.group( |
||||
|
{ |
||||
|
id: [this.property.id, [Validators.required]], |
||||
|
name: [this.property.name, [Validators.required]], |
||||
|
group: [this.property.group, []], |
||||
|
type: [this.property.type, [Validators.required]], |
||||
|
arrayItemType: [this.property.arrayItemType, [Validators.required]], |
||||
|
arrayItemName: [this.property.arrayItemName, []], |
||||
|
default: [this.property.default, []], |
||||
|
required: [this.property.required, []], |
||||
|
subLabel: [this.property.subLabel, []], |
||||
|
divider: [this.property.divider, []], |
||||
|
fieldSuffix: [this.property.fieldSuffix, []], |
||||
|
disableOnProperty: [this.property.disableOnProperty, []], |
||||
|
condition: [this.property.condition, []], |
||||
|
rowClass: [this.property.rowClass ? this.property.rowClass.split(' ') : [], []], |
||||
|
fieldClass: [this.property.fieldClass ? this.property.fieldClass.split(' ') : [], []], |
||||
|
rows: [this.property.rows, [Validators.min(1)]], |
||||
|
min: [this.property.min, []], |
||||
|
max: [this.property.max, []], |
||||
|
step: [this.property.step, [Validators.min(0)]], |
||||
|
properties: [this.property.properties, []], |
||||
|
multiple: [this.property.multiple, []], |
||||
|
allowEmptyOption: [this.property.allowEmptyOption, []], |
||||
|
minItems: [this.property.minItems, []], |
||||
|
maxItems: [this.property.maxItems, []], |
||||
|
items: [this.property.items, []], |
||||
|
helpId: [this.property.helpId, []], |
||||
|
direction: [this.property.direction || 'column', []], |
||||
|
allowClear: [this.property.allowClear || true, []], |
||||
|
dateTimeType: [this.property.dateTimeType || 'datetime', []], |
||||
|
htmlContent: [this.property.htmlContent || '', []], |
||||
|
htmlClassList: [this.property.htmlClassList || [], []] |
||||
|
} |
||||
|
); |
||||
|
if (this.disabled) { |
||||
|
this.propertyFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('type').valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(() => { |
||||
|
this.updateValidators(); |
||||
|
}); |
||||
|
this.propertyFormGroup.get('arrayItemType').valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(() => { |
||||
|
this.updateValidators(); |
||||
|
}); |
||||
|
this.propertyFormGroup.get('items').valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(() => { |
||||
|
this.onSelectItemsChange(); |
||||
|
}); |
||||
|
this.propertyFormGroup.get('multiple').valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(() => { |
||||
|
this.onMultipleSelectChange(); |
||||
|
}); |
||||
|
this.updateValidators(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.popover?.hide(); |
||||
|
} |
||||
|
|
||||
|
applyPropertySettings() { |
||||
|
const property = this.propertyFormGroup.getRawValue(); |
||||
|
property.rowClass = (property.rowClass || []).join(' '); |
||||
|
property.fieldClass = (property.fieldClass || []).join(' '); |
||||
|
this.propertySettingsApplied.emit(property); |
||||
|
} |
||||
|
|
||||
|
private updateValidators() { |
||||
|
if (this.isArray) { |
||||
|
this.propertyFormGroup.get('arrayItemType').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('arrayItemName').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('arrayItemType').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('arrayItemName').disable({emitEvent: false}); |
||||
|
} |
||||
|
const type = this.propertyItemType; |
||||
|
if (type === FormPropertyType.textarea) { |
||||
|
this.propertyFormGroup.get('rows').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('rows').disable({emitEvent: false}); |
||||
|
} |
||||
|
if (type === FormPropertyType.number) { |
||||
|
this.propertyFormGroup.get('min').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('max').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('step').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('min').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('max').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('step').disable({emitEvent: false}); |
||||
|
} |
||||
|
if (type === FormPropertyType.fieldset) { |
||||
|
this.propertyFormGroup.get('default').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('properties').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('default').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('properties').disable({emitEvent: false}); |
||||
|
} |
||||
|
if ([FormPropertyType.select, FormPropertyType.radios].includes(type)) { |
||||
|
this.propertyFormGroup.get('items').enable({emitEvent: false}); |
||||
|
if (type === FormPropertyType.select) { |
||||
|
this.propertyFormGroup.get('multiple').enable({emitEvent: false}); |
||||
|
const multiple: boolean = this.propertyFormGroup.get('multiple').value; |
||||
|
if (multiple) { |
||||
|
this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('minItems').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('maxItems').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('allowEmptyOption').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('minItems').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('maxItems').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('multiple').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('minItems').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('maxItems').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('items').disable({emitEvent: false}); |
||||
|
} |
||||
|
if (type === FormPropertyType.datetime) { |
||||
|
this.propertyFormGroup.get('allowClear').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('dateTimeType').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('allowClear').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('dateTimeType').disable({emitEvent: false}); |
||||
|
} |
||||
|
if (type === FormPropertyType.htmlSection) { |
||||
|
this.propertyFormGroup.get('htmlContent').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('htmlClassList').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('htmlContent').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('htmlClassList').disable({emitEvent: false}); |
||||
|
} |
||||
|
if ([FormPropertyType.javascript, FormPropertyType.markdown].includes(type)) { |
||||
|
this.propertyFormGroup.get('helpId').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('helpId').disable({emitEvent: false}); |
||||
|
} |
||||
|
if (this.propertyType !== type) { |
||||
|
const defaultValue = defaultPropertyValue(type); |
||||
|
this.propertyFormGroup.get('default').patchValue(defaultValue, {emitEvent: false}); |
||||
|
this.propertyType = type; |
||||
|
if (type === FormPropertyType.textarea) { |
||||
|
if (isUndefinedOrNull(this.propertyFormGroup.get('rows').value)) { |
||||
|
this.propertyFormGroup.get('rows').patchValue(2, {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
if (type === FormPropertyType.radios) { |
||||
|
if (isUndefinedOrNull(this.propertyFormGroup.get('direction').value)) { |
||||
|
this.propertyFormGroup.get('direction').patchValue('column', {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
if (type === FormPropertyType.datetime) { |
||||
|
if (isUndefinedOrNull(this.propertyFormGroup.get('dateTimeType').value)) { |
||||
|
this.propertyFormGroup.get('dateTimeType').patchValue('datetime', {emitEvent: false}); |
||||
|
} |
||||
|
if (isUndefinedOrNull(this.propertyFormGroup.get('allowClear').value)) { |
||||
|
this.propertyFormGroup.get('allowClear').patchValue(true, {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onSelectItemsChange() { |
||||
|
const type = this.propertyItemType; |
||||
|
const multiple: boolean = this.propertyFormGroup.get('multiple').value; |
||||
|
const defaultValue: any = this.propertyFormGroup.get('default').value; |
||||
|
const items: FormSelectItem[] = this.propertyFormGroup.get('items').value; |
||||
|
if (defaultValue && [FormPropertyType.select, FormPropertyType.radios].includes(type)) { |
||||
|
if (multiple && FormPropertyType.select === type) { |
||||
|
let targetValue: any[] = defaultValue; |
||||
|
targetValue = targetValue.filter(valItem => !!items.find(item => item.value === valItem)); |
||||
|
this.propertyFormGroup.get('default').patchValue(targetValue, {emitEvent: false}); |
||||
|
} else { |
||||
|
if (!items.find(item => item.value === defaultValue)) { |
||||
|
this.propertyFormGroup.get('default').patchValue(null, {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onMultipleSelectChange() { |
||||
|
const type = this.propertyItemType; |
||||
|
const multiple: boolean = this.propertyFormGroup.get('multiple').value; |
||||
|
const defaultValue: any = this.propertyFormGroup.get('default').value; |
||||
|
if (type === FormPropertyType.select) { |
||||
|
if (multiple) { |
||||
|
if (defaultValue && !Array.isArray(defaultValue)) { |
||||
|
const newVal = [defaultValue]; |
||||
|
this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false}); |
||||
|
} |
||||
|
this.propertyFormGroup.get('allowEmptyOption').patchValue(false, {emitEvent: false}); |
||||
|
this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false}) |
||||
|
this.propertyFormGroup.get('minItems').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('maxItems').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
if (defaultValue && Array.isArray(defaultValue)) { |
||||
|
const newVal = defaultValue.length ? defaultValue[0] : null; |
||||
|
setTimeout(() => { |
||||
|
this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false}); |
||||
|
}); |
||||
|
} |
||||
|
this.propertyFormGroup.get('allowEmptyOption').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('minItems').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('maxItems').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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 [formGroup]="selectItemRowFormGroup" class="tb-form-table-row tb-dynamic-form-select-item-row"> |
||||
|
<tb-value-input class="tb-value-field" |
||||
|
shortBooleanField |
||||
|
[allowedValueTypes]="[ValueType.STRING, ValueType.INTEGER, ValueType.DOUBLE, ValueType.BOOLEAN]" |
||||
|
formControlName="value"> |
||||
|
</tb-value-input> |
||||
|
<mat-form-field class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input required matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
</mat-form-field> |
||||
|
<div class="tb-form-table-row-cell-buttons"> |
||||
|
<button *ngIf="!disabled" |
||||
|
type="button" |
||||
|
mat-icon-button |
||||
|
(click)="selectItemRemoved.emit()" |
||||
|
matTooltip="{{ 'dynamic-form.property.remove-option' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>delete</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,171 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
ChangeDetectorRef, |
||||
|
Component, DestroyRef, |
||||
|
EventEmitter, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
Output, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
Validator, |
||||
|
ValidatorFn, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { isDefinedAndNotNull } from '@core/utils'; |
||||
|
import { FormSelectItem } from '@shared/models/dynamic-form.models'; |
||||
|
import { |
||||
|
DynamicFormSelectItemsComponent |
||||
|
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component'; |
||||
|
import { ValueType } from '@shared/models/constants'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
|
||||
|
export const selectItemValid = (item: FormSelectItem): boolean => isDefinedAndNotNull(item.value) && !!item.label; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-dynamic-form-select-item-row', |
||||
|
templateUrl: './dynamic-form-select-item-row.component.html', |
||||
|
styleUrls: ['./dynamic-form-select-item-row.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => DynamicFormSelectItemRowComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => DynamicFormSelectItemRowComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class DynamicFormSelectItemRowComponent implements ControlValueAccessor, OnInit, Validator { |
||||
|
|
||||
|
ValueType = ValueType; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
index: number; |
||||
|
|
||||
|
@Output() |
||||
|
selectItemRemoved = new EventEmitter(); |
||||
|
|
||||
|
selectItemRowFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
modelValue: FormSelectItem; |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private destroyRef: DestroyRef, |
||||
|
private cd: ChangeDetectorRef, |
||||
|
private selectItemsComponent: DynamicFormSelectItemsComponent) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.selectItemRowFormGroup = this.fb.group({ |
||||
|
value: [null, [this.selectItemValueValidator()]], |
||||
|
label: [null, [Validators.required]] |
||||
|
}); |
||||
|
this.selectItemRowFormGroup.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe( |
||||
|
() => this.updateModel() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.selectItemRowFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.selectItemRowFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: FormSelectItem): void { |
||||
|
this.modelValue = value; |
||||
|
this.selectItemRowFormGroup.patchValue( |
||||
|
{ |
||||
|
value: value?.value, |
||||
|
label: value?.label |
||||
|
}, {emitEvent: false} |
||||
|
); |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
public validate(_c: UntypedFormControl) { |
||||
|
const valueControl = this.selectItemRowFormGroup.get('value'); |
||||
|
if (valueControl.hasError('itemValueNotUnique')) { |
||||
|
valueControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); |
||||
|
} |
||||
|
if (valueControl.hasError('itemValueNotUnique')) { |
||||
|
this.selectItemRowFormGroup.get('value').markAsTouched(); |
||||
|
return { |
||||
|
itemValueNotUnique: true |
||||
|
}; |
||||
|
} |
||||
|
const item: FormSelectItem = {...this.modelValue, ...this.selectItemRowFormGroup.value}; |
||||
|
if (!selectItemValid(item)) { |
||||
|
return { |
||||
|
selectItem: true |
||||
|
}; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private selectItemValueValidator(): ValidatorFn { |
||||
|
return control => { |
||||
|
if (!control.value) { |
||||
|
return { |
||||
|
required: true |
||||
|
}; |
||||
|
} |
||||
|
if (!this.selectItemsComponent.selectItemValueUnique(control.value, this.index)) { |
||||
|
return { |
||||
|
itemValueNotUnique: true |
||||
|
}; |
||||
|
} |
||||
|
return null; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
const value: FormSelectItem = this.selectItemRowFormGroup.value; |
||||
|
this.modelValue = {...this.modelValue, ...value}; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,204 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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, DestroyRef, |
||||
|
forwardRef, |
||||
|
HostBinding, |
||||
|
Input, |
||||
|
OnInit, |
||||
|
QueryList, |
||||
|
ViewChildren, |
||||
|
ViewEncapsulation |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
AbstractControl, |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormArray, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
Validator |
||||
|
} from '@angular/forms'; |
||||
|
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { FormSelectItem } from '@shared/models/dynamic-form.models'; |
||||
|
import { |
||||
|
DynamicFormSelectItemRowComponent, |
||||
|
selectItemValid |
||||
|
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-dynamic-form-select-items', |
||||
|
templateUrl: './dynamic-form-select-items.component.html', |
||||
|
styleUrls: ['./dynamic-form-select-items.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => DynamicFormSelectItemsComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => DynamicFormSelectItemsComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
encapsulation: ViewEncapsulation.None |
||||
|
}) |
||||
|
export class DynamicFormSelectItemsComponent implements ControlValueAccessor, OnInit, Validator { |
||||
|
|
||||
|
@HostBinding('style.display') styleDisplay = 'flex'; |
||||
|
@HostBinding('style.overflow') styleOverflow = 'hidden'; |
||||
|
|
||||
|
@ViewChildren(DynamicFormSelectItemRowComponent) |
||||
|
selectItemRows: QueryList<DynamicFormSelectItemRowComponent>; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
selectItemsFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
errorText = ''; |
||||
|
|
||||
|
get dragEnabled(): boolean { |
||||
|
return !this.disabled && this.selectItemsFormArray().controls.length > 1; |
||||
|
} |
||||
|
|
||||
|
private propagateChange = (_val: any) => {}; |
||||
|
|
||||
|
constructor(private fb: UntypedFormBuilder, |
||||
|
private destroyRef: DestroyRef, |
||||
|
private translate: TranslateService) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.selectItemsFormGroup = this.fb.group({ |
||||
|
selectItems: this.fb.array([]) |
||||
|
}); |
||||
|
this.selectItemsFormGroup.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe( |
||||
|
() => { |
||||
|
let items: FormSelectItem[] = this.selectItemsFormGroup.get('selectItems').value; |
||||
|
if (items) { |
||||
|
items = items.filter(i => selectItemValid(i)); |
||||
|
} |
||||
|
this.propagateChange(items); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.selectItemsFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.selectItemsFormGroup.enable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: FormSelectItem[] | undefined): void { |
||||
|
const items= value || []; |
||||
|
this.selectItemsFormGroup.setControl('selectItems', this.prepareSelectItemsFormArray(items), {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
public validate(_c: UntypedFormControl) { |
||||
|
this.errorText = ''; |
||||
|
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray; |
||||
|
const notUniqueControls = |
||||
|
itemsArray.controls.filter(control => control.hasError('itemValueNotUnique')); |
||||
|
for (const control of notUniqueControls) { |
||||
|
control.updateValueAndValidity({onlySelf: false, emitEvent: false}); |
||||
|
if (control.hasError('itemValueNotUnique')) { |
||||
|
this.errorText = this.translate.instant('dynamic-form.property.not-unique-select-option-value-error'); |
||||
|
} |
||||
|
} |
||||
|
let valid = this.selectItemsFormGroup.valid; |
||||
|
if (valid) { |
||||
|
const items: FormSelectItem[] = this.selectItemsFormGroup.get('selectItems').value; |
||||
|
valid = !items.some(item => !selectItemValid(item)); |
||||
|
} |
||||
|
|
||||
|
return valid ? null : { |
||||
|
selectItems: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public selectItemValueUnique(value: any, index: number): boolean { |
||||
|
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray; |
||||
|
for (let i = 0; i < itemsArray.controls.length; i++) { |
||||
|
if (i !== index) { |
||||
|
const otherControl = itemsArray.controls[i]; |
||||
|
if (value === otherControl.value.value) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
selectItemDrop(event: CdkDragDrop<string[]>) { |
||||
|
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray; |
||||
|
const item = itemsArray.at(event.previousIndex); |
||||
|
itemsArray.removeAt(event.previousIndex, {emitEvent: false}); |
||||
|
itemsArray.insert(event.currentIndex, item, {emitEvent: true}); |
||||
|
} |
||||
|
|
||||
|
selectItemsFormArray(): UntypedFormArray { |
||||
|
return this.selectItemsFormGroup.get('selectItems') as UntypedFormArray; |
||||
|
} |
||||
|
|
||||
|
trackBySelectItem(_index: number, selectItemControl: AbstractControl): any { |
||||
|
return selectItemControl; |
||||
|
} |
||||
|
|
||||
|
removeSelectItem(index: number, emitEvent = true) { |
||||
|
(this.selectItemsFormGroup.get('selectItems') as UntypedFormArray).removeAt(index, {emitEvent}); |
||||
|
} |
||||
|
|
||||
|
addSelectItem() { |
||||
|
const item: FormSelectItem = { |
||||
|
value: '', |
||||
|
label: '' |
||||
|
}; |
||||
|
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray; |
||||
|
const itemControl = this.fb.control(item, []); |
||||
|
itemsArray.push(itemControl); |
||||
|
} |
||||
|
|
||||
|
private prepareSelectItemsFormArray(items: FormSelectItem[] | undefined): UntypedFormArray { |
||||
|
const selectItemsControls: Array<AbstractControl> = []; |
||||
|
if (items) { |
||||
|
items.forEach((item) => { |
||||
|
selectItemsControls.push(this.fb.control(item, [])); |
||||
|
}); |
||||
|
} |
||||
|
return this.fb.array(selectItemsControls); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,266 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 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.!hidden]="!propertyGroups?.length && !propertiesContent.children?.length" class="tb-form-panel" |
||||
|
[class.stroked]="stroked" [class.no-padding]="noPadding" [class.no-border]="noBorder" |
||||
|
[formGroup]="propertiesFormGroup"> |
||||
|
<div *ngIf="title" class="tb-form-panel-title">{{ title }}</div> |
||||
|
<ng-container #propertiesContent><ng-content select=".tb-properties-content"></ng-content></ng-container> |
||||
|
<ng-container *ngFor="let propertyGroup of propertyGroups"> |
||||
|
<ng-container *ngIf="propertyGroup.visible"> |
||||
|
<div *ngIf="propertyGroup.title" class="tb-form-panel stroked"> |
||||
|
<mat-expansion-panel class="tb-settings" expanded> |
||||
|
<mat-expansion-panel-header class="flex flex-row flex-wrap"> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-form-panel-title">{{ propertyGroup.title | customTranslate }}</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<ng-template matExpansionPanelContent> |
||||
|
<ng-container *ngTemplateOutlet="propertyContainersTpl; context:{propertyContainers: propertyGroup.containers}"></ng-container> |
||||
|
</ng-template> |
||||
|
</mat-expansion-panel> |
||||
|
</div> |
||||
|
<ng-container *ngIf="!propertyGroup.title"> |
||||
|
<ng-container *ngTemplateOutlet="propertyContainersTpl; context:{propertyContainers: propertyGroup.containers}"></ng-container> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
|
||||
|
<ng-template #propertyContainersTpl let-propertyContainers="propertyContainers"> |
||||
|
<ng-container *ngFor="let propertyContainer of propertyContainers" [formGroup]="propertiesFormGroup"> |
||||
|
<ng-container *ngIf="propertyContainer.visible"> |
||||
|
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.field"> |
||||
|
<ng-container *ngTemplateOutlet="propertyFieldTpl;context:{property: propertyContainer.property}"> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.row"> |
||||
|
<ng-container *ngTemplateOutlet="propertyRowTpl;context:{propertyRow: propertyContainer}"> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
<tb-dynamic-form *ngIf="propertyContainer.type === FormPropertyContainerType.fieldset" |
||||
|
formControlName="{{ propertyContainer.property.id }}" |
||||
|
title="{{ propertyContainer.label | customTranslate }}" |
||||
|
stroked |
||||
|
[properties]="propertyContainer.properties"> |
||||
|
</tb-dynamic-form> |
||||
|
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.array"> |
||||
|
<div *ngIf="propertyContainer.arrayItemProperty.type === FormPropertyType.image" class="tb-form-row flex-1"> |
||||
|
<tb-multiple-gallery-image-input |
||||
|
class="flex-1" |
||||
|
label="{{ propertyContainer.property.name | customTranslate }}" |
||||
|
formControlName="{{ propertyContainer.property.id }}" |
||||
|
[required]="propertyContainer.property.required"> |
||||
|
</tb-multiple-gallery-image-input> |
||||
|
</div> |
||||
|
<tb-dynamic-form-array *ngIf="propertyContainer.arrayItemProperty.type !== FormPropertyType.image" |
||||
|
formControlName="{{ propertyContainer.property.id }}" |
||||
|
[itemProperty]="propertyContainer.arrayItemProperty" |
||||
|
title="{{ propertyContainer.label | customTranslate }}"> |
||||
|
</tb-dynamic-form-array> |
||||
|
</ng-container> |
||||
|
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.htmlSection"> |
||||
|
<div [innerHTML]="propertyContainer.safeHtml" [class]="propertyContainer.htmlClass"></div> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #propertyFieldTpl let-property="property"> |
||||
|
<div [formGroup]="propertiesFormGroup" class="tb-form-row no-border no-padding tb-standard-fields"> |
||||
|
<ng-container *ngTemplateOutlet="inputFieldTpl;context:{property: property, singleField: true}"> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #propertyRowTpl let-propertyRow="propertyRow"> |
||||
|
<div [formGroup]="propertiesFormGroup" class="tb-form-row space-between overflow-auto" [class]="propertyRow.rowClass"> |
||||
|
<mat-slide-toggle *ngIf="propertyRow.switch && propertyRow.switch.visible" |
||||
|
class="mat-slide fixed-title-width margin" formControlName="{{ propertyRow.switch.id }}"> |
||||
|
{{ propertyRow.label | customTranslate }} |
||||
|
</mat-slide-toggle> |
||||
|
<div *ngIf="!propertyRow.switch" class="fixed-title-width fixed-title-height">{{ propertyRow.label | customTranslate }}</div> |
||||
|
<div *ngIf="propertyRow.properties.length" class="tb-flex" [class]="propertyRow.propertiesRowClass"> |
||||
|
<ng-container *ngFor="let property of propertyRow.properties"> |
||||
|
<ng-container *ngIf="property.visible"> |
||||
|
<div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel | customTranslate }}</div> |
||||
|
<ng-container *ngIf="isInputFieldPropertyType(property.type)"> |
||||
|
<ng-container *ngTemplateOutlet="inputFieldTpl;context:{property: property, singleField: false}"> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
<tb-color-input *ngIf="property.type === FormPropertyType.color" |
||||
|
[required]="property.required" |
||||
|
[class]="property.fieldClass" |
||||
|
asBoxInput |
||||
|
colorClearButton |
||||
|
formControlName="{{ property.id }}"> |
||||
|
</tb-color-input> |
||||
|
<tb-color-settings *ngIf="property.type === FormPropertyType.color_settings" |
||||
|
[class]="property.fieldClass" |
||||
|
formControlName="{{ property.id }}" |
||||
|
settingsKey="{{ property.name | customTranslate }}"> |
||||
|
</tb-color-settings> |
||||
|
<tb-font-settings *ngIf="property.type === FormPropertyType.font" |
||||
|
[class]="property.fieldClass" |
||||
|
formControlName="{{ property.id }}" |
||||
|
clearButton |
||||
|
disabledLineHeight> |
||||
|
</tb-font-settings> |
||||
|
<tb-unit-input *ngIf="property.type === FormPropertyType.units" |
||||
|
[class]="property.fieldClass" |
||||
|
[required]="property.required" |
||||
|
formControlName="{{ property.id }}"></tb-unit-input> |
||||
|
<tb-material-icon-select *ngIf="property.type === FormPropertyType.icon" |
||||
|
[required]="property.required" |
||||
|
asBoxInput |
||||
|
formControlName="{{ property.id }}"> |
||||
|
</tb-material-icon-select> |
||||
|
<mat-divider *ngIf="property.divider" vertical></mat-divider> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #inputFieldTpl let-property="property" let-singleField="singleField"> |
||||
|
<ng-container [formGroup]="propertiesFormGroup"> |
||||
|
<mat-form-field *ngIf="property.type === FormPropertyType.textarea" |
||||
|
[class]="singleField ? 'flex' : property.fieldClass" |
||||
|
[floatLabel]="singleField ? 'always' : 'auto'" |
||||
|
appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label> |
||||
|
<textarea matInput |
||||
|
[rows]="property.rows || 2" |
||||
|
formControlName="{{ property.id }}" |
||||
|
[required]="property.required" |
||||
|
placeholder="{{ 'widget-config.set' | translate }}"></textarea> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field *ngIf="[FormPropertyType.text, FormPropertyType.password].includes(property.type)" |
||||
|
[class]="singleField ? 'flex' : property.fieldClass" |
||||
|
[floatLabel]="singleField ? 'always' : 'auto'" |
||||
|
appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label> |
||||
|
<input matInput |
||||
|
[type]="property.type" |
||||
|
formControlName="{{ property.id }}" |
||||
|
[required]="property.required" |
||||
|
placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field *ngIf="property.type === FormPropertyType.number" |
||||
|
[class]="singleField ? 'flex' : property.fieldClass" |
||||
|
class="number" |
||||
|
[floatLabel]="singleField ? 'always' : 'auto'" |
||||
|
appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label> |
||||
|
<input matInput |
||||
|
type="number" |
||||
|
formControlName="{{ property.id }}" |
||||
|
[required]="property.required" |
||||
|
[min]="property.min" |
||||
|
[max]="property.max" |
||||
|
[step]="property.step" |
||||
|
placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field *ngIf="property.type === FormPropertyType.select" |
||||
|
[class]="singleField ? 'flex' : (property.fieldClass || 'medium-width')" |
||||
|
[floatLabel]="singleField ? 'always' : 'auto'" |
||||
|
appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label> |
||||
|
<mat-select formControlName="{{ property.id }}" |
||||
|
[required]="property.required" |
||||
|
[multiple]="property.multiple"> |
||||
|
<mat-option *ngIf="!property.multiple && property.allowEmptyOption" [value]="null"></mat-option> |
||||
|
<mat-option *ngFor="let item of property.items" [value]="item.value"> |
||||
|
{{ item.label | customTranslate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<tb-datetime *ngIf="property.type === FormPropertyType.datetime" |
||||
|
[fieldClass]="singleField ? 'flex' : property.fieldClass" |
||||
|
[class.flex-1]="singleField || property.fieldClass?.includes('flex')" |
||||
|
[floatLabel]="singleField ? 'always' : 'auto'" |
||||
|
[showLabel]="singleField" |
||||
|
[dateText]="singleField ? (property.name | customTranslate) : ''" |
||||
|
[allowClear]="property.allowClear" |
||||
|
[required]="property.required" |
||||
|
appearance="outline" |
||||
|
subscriptSizing="dynamic" |
||||
|
[type]="property.dateTimeType" |
||||
|
formControlName="{{ property.id }}"> |
||||
|
</tb-datetime> |
||||
|
<div *ngIf="property.type === FormPropertyType.javascript" class="tb-form-row flex-1"> |
||||
|
<tb-js-func class="flex-1" |
||||
|
label="{{ property.name | customTranslate }}" |
||||
|
disableUndefinedCheck |
||||
|
[required]="property.required" |
||||
|
[helpId]="property.helpId" |
||||
|
formControlName="{{ property.id }}"> |
||||
|
</tb-js-func> |
||||
|
</div> |
||||
|
<div *ngIf="property.type === FormPropertyType.json" class="tb-form-row flex-1"> |
||||
|
<tb-json-content class="flex-1" |
||||
|
label="{{ property.name | customTranslate }}" |
||||
|
[required]="property.required" |
||||
|
[contentType]="ContentType.JSON" |
||||
|
formControlName="{{ property.id }}"> |
||||
|
</tb-json-content> |
||||
|
</div> |
||||
|
<div *ngIf="property.type === FormPropertyType.html" class="tb-form-row flex-1"> |
||||
|
<tb-html class="flex-1" |
||||
|
label="{{ property.name | customTranslate }}" |
||||
|
[required]="property.required" |
||||
|
formControlName="{{ property.id }}"> |
||||
|
</tb-html> |
||||
|
</div> |
||||
|
<div *ngIf="property.type === FormPropertyType.css" class="tb-form-row flex-1"> |
||||
|
<tb-css class="flex-1" |
||||
|
label="{{ property.name | customTranslate }}" |
||||
|
[required]="property.required" |
||||
|
formControlName="{{ property.id }}"> |
||||
|
</tb-css> |
||||
|
</div> |
||||
|
<div *ngIf="property.type === FormPropertyType.markdown" class="tb-form-row flex-1"> |
||||
|
<tb-markdown-editor class="flex-1" |
||||
|
label="{{ property.name | customTranslate }}" |
||||
|
[required]="property.required" |
||||
|
[helpId]="property.helpId" |
||||
|
formControlName="{{ property.id }}"> |
||||
|
</tb-markdown-editor> |
||||
|
</div> |
||||
|
<div *ngIf="property.type === FormPropertyType.image" class="tb-form-row flex-1"> |
||||
|
<tb-gallery-image-input |
||||
|
class="flex-1" |
||||
|
label="{{ property.name | customTranslate }}" |
||||
|
formControlName="{{ property.id }}" |
||||
|
[required]="property.required"> |
||||
|
</tb-gallery-image-input> |
||||
|
</div> |
||||
|
<div *ngIf="property.type === FormPropertyType.radios" |
||||
|
class="tb-form-row tb-radios-property column-xs flex-1" [class.direction-column]="property.direction === 'column'"> |
||||
|
<div tbTruncateWithTooltip class="fixed-title-width fixed-title-height">{{ property.name | customTranslate }}</div> |
||||
|
<mat-radio-group class="flex flex-1 flex-wrap" [class.flex-col]="property.direction === 'column'" |
||||
|
[required]="property.required" |
||||
|
formControlName="{{ property.id }}"> |
||||
|
<mat-radio-button *ngFor="let item of property.items" [value]="item.value"> |
||||
|
<div tbTruncateWithTooltip>{{ item.label | customTranslate }}</div> |
||||
|
</mat-radio-button> |
||||
|
</mat-radio-group> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
@ -0,0 +1,301 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 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 { |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
DestroyRef, |
||||
|
forwardRef, |
||||
|
Input, |
||||
|
OnChanges, |
||||
|
OnInit, |
||||
|
SimpleChanges |
||||
|
} from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormBuilder, |
||||
|
UntypedFormControl, |
||||
|
UntypedFormGroup, |
||||
|
Validator, |
||||
|
ValidatorFn, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; |
||||
|
import { |
||||
|
defaultFormProperties, |
||||
|
FormProperty, |
||||
|
FormPropertyContainerType, |
||||
|
FormPropertyGroup, |
||||
|
FormPropertyType, |
||||
|
isInputFieldPropertyType, |
||||
|
PropertyConditionFunction, |
||||
|
toPropertyGroups |
||||
|
} from '@shared/models/dynamic-form.models'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; |
||||
|
import { ContentType } from '@shared/models/constants'; |
||||
|
import { DomSanitizer } from '@angular/platform-browser'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-dynamic-form', |
||||
|
templateUrl: './dynamic-form.component.html', |
||||
|
styleUrls: ['./dynamic-form.component.scss'], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => DynamicFormComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => DynamicFormComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { |
||||
|
|
||||
|
isInputFieldPropertyType = isInputFieldPropertyType; |
||||
|
|
||||
|
FormPropertyContainerType = FormPropertyContainerType; |
||||
|
|
||||
|
FormPropertyType = FormPropertyType; |
||||
|
|
||||
|
ContentType = ContentType; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
properties: FormProperty[]; |
||||
|
|
||||
|
@Input() |
||||
|
title: string; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
isArrayItem = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
stroked = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
noPadding = false; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
noBorder = false; |
||||
|
|
||||
|
private modelValue: {[id: string]: any}; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
private validatorTriggers: string[]; |
||||
|
|
||||
|
public propertiesFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
propertyGroups: FormPropertyGroup[]; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
private customTranslate: CustomTranslatePipe, |
||||
|
private sanitizer: DomSanitizer, |
||||
|
private destroyRef: DestroyRef, |
||||
|
private fb: UntypedFormBuilder, |
||||
|
private cd: ChangeDetectorRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.propertiesFormGroup = this.fb.group({ |
||||
|
}); |
||||
|
this.propertiesFormGroup.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
this.loadMetadata(); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges): void { |
||||
|
for (const propName of Object.keys(changes)) { |
||||
|
const change = changes[propName]; |
||||
|
if (!change.firstChange && change.currentValue !== change.previousValue) { |
||||
|
if (['properties'].includes(propName)) { |
||||
|
this.loadMetadata(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(_fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
if (isDisabled) { |
||||
|
this.propertiesFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertiesFormGroup.enable({emitEvent: false}); |
||||
|
this.updateControlsState(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeValue(value: {[id: string]: any}): void { |
||||
|
this.modelValue = value || {}; |
||||
|
this.setupValue(); |
||||
|
} |
||||
|
|
||||
|
validate(_c: UntypedFormControl) { |
||||
|
const valid = this.propertiesFormGroup.valid; |
||||
|
return valid ? null : { |
||||
|
properties: { |
||||
|
valid: false, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private loadMetadata() { |
||||
|
this.validatorTriggers = []; |
||||
|
this.propertyGroups = []; |
||||
|
|
||||
|
for (const control of Object.keys(this.propertiesFormGroup.controls)) { |
||||
|
this.propertiesFormGroup.removeControl(control, {emitEvent: false}); |
||||
|
} |
||||
|
if (this.properties) { |
||||
|
for (let property of this.properties) { |
||||
|
property.disabled = false; |
||||
|
property.visible = true; |
||||
|
if (property.condition) { |
||||
|
try { |
||||
|
property.conditionFunction = new Function('property', 'model', property.condition) as PropertyConditionFunction; |
||||
|
} catch (_e) { |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
this.propertyGroups = toPropertyGroups(this.properties, this.isArrayItem, this.customTranslate, this.sanitizer); |
||||
|
for (const property of this.properties) { |
||||
|
if (property.type !== FormPropertyType.htmlSection) { |
||||
|
if (property.disableOnProperty) { |
||||
|
if (!this.validatorTriggers.includes(property.disableOnProperty)) { |
||||
|
this.validatorTriggers.push(property.disableOnProperty); |
||||
|
} |
||||
|
} |
||||
|
const validators: ValidatorFn[] = []; |
||||
|
if (property.required) { |
||||
|
validators.push(Validators.required); |
||||
|
} |
||||
|
if (property.type === FormPropertyType.number) { |
||||
|
if (isDefinedAndNotNull(property.min)) { |
||||
|
validators.push(Validators.min(property.min)); |
||||
|
} |
||||
|
if (isDefinedAndNotNull(property.max)) { |
||||
|
validators.push(Validators.max(property.max)); |
||||
|
} |
||||
|
} |
||||
|
if (property.type === FormPropertyType.select) { |
||||
|
if (property.multiple) { |
||||
|
if (isDefinedAndNotNull(property.minItems)) { |
||||
|
validators.push(Validators.minLength(property.minItems)); |
||||
|
} |
||||
|
if (isDefinedAndNotNull(property.maxItems)) { |
||||
|
validators.push(Validators.maxLength(property.maxItems)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
this.propertiesFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
this.setupValue(); |
||||
|
this.cd.markForCheck(); |
||||
|
} |
||||
|
|
||||
|
private calculateControlsState(updateControls = false) { |
||||
|
for (const trigger of this.validatorTriggers) { |
||||
|
const value: boolean = this.propertiesFormGroup.get(trigger).value; |
||||
|
this.properties.filter(p => p.disableOnProperty === trigger).forEach( |
||||
|
(p) => { |
||||
|
p.disabled = !value; |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
if (this.properties) { |
||||
|
for (let property of this.properties) { |
||||
|
if (property.conditionFunction) { |
||||
|
property.visible = property.conditionFunction(property, this.modelValue); |
||||
|
} |
||||
|
} |
||||
|
this.propertyGroups.forEach(g => { |
||||
|
g.containers.forEach(container => { |
||||
|
if ([FormPropertyContainerType.fieldset, FormPropertyContainerType.field, FormPropertyContainerType.htmlSection, FormPropertyContainerType.array].includes(container.type)) { |
||||
|
container.visible = container.property.visible; |
||||
|
} else { |
||||
|
container.visible = container.switch?.visible || container.properties.some(p => p.visible); |
||||
|
} |
||||
|
}); |
||||
|
g.visible = g.containers.some(c => c.visible); |
||||
|
}); |
||||
|
} |
||||
|
if (updateControls) { |
||||
|
this.updateControlsState(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateControlsState() { |
||||
|
if (this.properties) { |
||||
|
for (let property of this.properties) { |
||||
|
if (property.type !== FormPropertyType.htmlSection) { |
||||
|
const control = this.propertiesFormGroup.get(property.id); |
||||
|
if (property.visible && !property.disabled) { |
||||
|
control.enable({emitEvent: false}); |
||||
|
control.updateValueAndValidity({emitEvent: false}); |
||||
|
} else { |
||||
|
control.disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private setupValue() { |
||||
|
if (this.properties) { |
||||
|
const defaults = defaultFormProperties(this.properties); |
||||
|
this.modelValue = mergeDeep<{[id: string]: any}>(defaults, this.modelValue); |
||||
|
this.propertiesFormGroup.patchValue( |
||||
|
this.modelValue, {emitEvent: false} |
||||
|
); |
||||
|
this.calculateControlsState(); |
||||
|
this.setDisabledState(this.disabled); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
this.modelValue = this.propertiesFormGroup.getRawValue(); |
||||
|
this.calculateControlsState(true); |
||||
|
this.propagateChange(this.modelValue); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,190 +0,0 @@ |
|||||
<!-- |
|
||||
|
|
||||
Copyright © 2016-2024 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-scada-symbol-property-settings-panel" [formGroup]="propertyFormGroup"> |
|
||||
<div class="tb-scada-symbol-property-settings-title">{{ panelTitle | translate }}</div> |
|
||||
<div class="tb-scada-symbol-property-settings-panel-content"> |
|
||||
<div class="tb-form-row"> |
|
||||
<div class="fixed-title-width" translate>scada.property.id</div> |
|
||||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|
||||
<input required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
<div class="tb-form-row"> |
|
||||
<div class="fixed-title-width" translate>scada.property.name</div> |
|
||||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|
||||
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
<div class="tb-form-row"> |
|
||||
<div class="fixed-title-width" translate>scada.property.type</div> |
|
||||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|
||||
<mat-select formControlName="type"> |
|
||||
<mat-option *ngFor="let type of scadaSymbolPropertyTypes" [value]="type"> |
|
||||
{{ scadaSymbolPropertyTypeTranslations.get(type) | translate }} |
|
||||
</mat-option> |
|
||||
</mat-select> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
<div class="tb-form-row space-between"> |
|
||||
<div class="fixed-title-width" translate>scada.property.default-value</div> |
|
||||
<mat-form-field *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.text" |
|
||||
class="flex" |
|
||||
appearance="outline" subscriptSizing="dynamic"> |
|
||||
<input matInput formControlName="default" placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
</mat-form-field> |
|
||||
<tb-color-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.color" |
|
||||
asBoxInput |
|
||||
colorClearButton |
|
||||
formControlName="default"> |
|
||||
</tb-color-input> |
|
||||
<tb-color-settings *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.color_settings" |
|
||||
formControlName="default" |
|
||||
settingsKey="{{ propertyFormGroup.get('id').value }}"> |
|
||||
</tb-color-settings> |
|
||||
<mat-form-field *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.number" |
|
||||
appearance="outline" |
|
||||
class="number" |
|
||||
subscriptSizing="dynamic"> |
|
||||
<input matInput formControlName="default" |
|
||||
[min]="propertyFormGroup.get('min').value" |
|
||||
[max]="propertyFormGroup.get('max').value" |
|
||||
[step]="propertyFormGroup.get('step').value" |
|
||||
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
</mat-form-field> |
|
||||
<tb-font-settings *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.font" |
|
||||
formControlName="default" |
|
||||
clearButton |
|
||||
disabledLineHeight> |
|
||||
</tb-font-settings> |
|
||||
<tb-unit-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.units" |
|
||||
formControlName="default"></tb-unit-input> |
|
||||
<tb-value-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.switch" |
|
||||
[valueType]="ValueType.BOOLEAN" |
|
||||
formControlName="default"></tb-value-input> |
|
||||
<tb-material-icon-select *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.icon" |
|
||||
asBoxInput |
|
||||
formControlName="default"> |
|
||||
</tb-material-icon-select> |
|
||||
</div> |
|
||||
<div *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.number" class="tb-form-row space-between"> |
|
||||
<div translate>scada.property.number-settings</div> |
|
||||
<div class="flex flex-1 flex-row items-center justify-end gap-2"> |
|
||||
<div class="tb-small-label" translate>scada.property.min</div> |
|
||||
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic"> |
|
||||
<input matInput formControlName="min" |
|
||||
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
</mat-form-field> |
|
||||
<mat-divider vertical></mat-divider> |
|
||||
<div class="tb-small-label" translate>scada.property.max</div> |
|
||||
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic"> |
|
||||
<input matInput formControlName="max" |
|
||||
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
</mat-form-field> |
|
||||
<mat-divider vertical></mat-divider> |
|
||||
<div class="tb-small-label" translate>scada.property.step</div> |
|
||||
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic"> |
|
||||
<input matInput formControlName="step" min="0" |
|
||||
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="tb-form-row space-between"> |
|
||||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="required"> |
|
||||
{{ 'scada.property.value-required' | translate }} |
|
||||
</mat-slide-toggle> |
|
||||
</div> |
|
||||
<div class="tb-form-panel stroked"> |
|
||||
<mat-expansion-panel class="tb-settings"> |
|
||||
<mat-expansion-panel-header class="flex flex-row flex-wrap"> |
|
||||
<mat-panel-title> |
|
||||
{{ 'scada.property.advanced-ui-settings' | translate }} |
|
||||
</mat-panel-title> |
|
||||
</mat-expansion-panel-header> |
|
||||
<ng-template matExpansionPanelContent> |
|
||||
<div class="tb-form-row space-between"> |
|
||||
<div class="fixed-title-width" translate>scada.property.sub-label</div> |
|
||||
<mat-form-field class="flex" |
|
||||
appearance="outline" subscriptSizing="dynamic"> |
|
||||
<input matInput formControlName="subLabel" placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
<div class="tb-form-row space-between"> |
|
||||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="divider"> |
|
||||
{{ 'scada.property.vertical-divider-after' | translate }} |
|
||||
</mat-slide-toggle> |
|
||||
</div> |
|
||||
<div *ngIf="[ScadaSymbolPropertyType.number, ScadaSymbolPropertyType.text].includes(propertyFormGroup.get('type').value)" |
|
||||
class="tb-form-row space-between"> |
|
||||
<div class="fixed-title-width" translate>scada.property.input-field-suffix</div> |
|
||||
<mat-form-field class="flex" |
|
||||
appearance="outline" subscriptSizing="dynamic"> |
|
||||
<input matInput formControlName="fieldSuffix" placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
<div class="tb-form-row space-between"> |
|
||||
<div class="fixed-title-width" translate>scada.property.disable-on-property</div> |
|
||||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|
||||
<mat-select formControlName="disableOnProperty" placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
<mat-option [value]="null"></mat-option> |
|
||||
<mat-option *ngFor="let prop of booleanPropertyIds" [value]="prop"> |
|
||||
{{ prop }} |
|
||||
</mat-option> |
|
||||
</mat-select> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
<div class="tb-form-row"> |
|
||||
<div class="fixed-title-width" translate>scada.property.property-row-classes</div> |
|
||||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|
||||
<mat-select formControlName="rowClass" multiple placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
<mat-option *ngFor="let clazz of scadaSymbolPropertyRowClasses" [value]="clazz"> |
|
||||
{{ clazz }} |
|
||||
</mat-option> |
|
||||
</mat-select> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
<div *ngIf="propertyFormGroup.get('type').value !== ScadaSymbolPropertyType.switch" class="tb-form-row"> |
|
||||
<div class="fixed-title-width" translate>scada.property.property-field-classes</div> |
|
||||
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> |
|
||||
<mat-select formControlName="fieldClass" multiple placeholder="{{ 'widget-config.set' | translate }}"> |
|
||||
<mat-option *ngFor="let clazz of scadaSymbolPropertyFieldClasses" [value]="clazz"> |
|
||||
{{ clazz }} |
|
||||
</mat-option> |
|
||||
</mat-select> |
|
||||
</mat-form-field> |
|
||||
</div> |
|
||||
</ng-template> |
|
||||
</mat-expansion-panel> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="tb-scada-symbol-property-settings-panel-buttons"> |
|
||||
<button mat-button |
|
||||
color="primary" |
|
||||
type="button" |
|
||||
(click)="cancel()"> |
|
||||
{{ (disabled ? 'action.close' : 'action.cancel') | translate }} |
|
||||
</button> |
|
||||
<button *ngIf="!disabled" |
|
||||
mat-raised-button |
|
||||
color="primary" |
|
||||
type="button" |
|
||||
(click)="applyPropertySettings()" |
|
||||
[disabled]="propertyFormGroup.invalid || !propertyFormGroup.dirty"> |
|
||||
{{ (isAdd ? 'action.add' : 'action.apply') | translate }} |
|
||||
</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
@ -1,140 +0,0 @@ |
|||||
///
|
|
||||
/// Copyright © 2016-2024 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, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; |
|
||||
import { TbPopoverComponent } from '@shared/components/popover.component'; |
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|
||||
import { |
|
||||
ScadaSymbolProperty, |
|
||||
scadaSymbolPropertyFieldClasses, |
|
||||
scadaSymbolPropertyRowClasses, |
|
||||
ScadaSymbolPropertyType, |
|
||||
scadaSymbolPropertyTypes, |
|
||||
scadaSymbolPropertyTypeTranslations |
|
||||
} from '@home/components/widget/lib/scada/scada-symbol.models'; |
|
||||
import { defaultPropertyValue } from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component'; |
|
||||
import { ValueType } from '@shared/models/constants'; |
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'tb-scada-symbol-property-panel', |
|
||||
templateUrl: './scada-symbol-property-panel.component.html', |
|
||||
styleUrls: ['./scada-symbol-property-panel.component.scss'], |
|
||||
encapsulation: ViewEncapsulation.None |
|
||||
}) |
|
||||
export class ScadaSymbolPropertyPanelComponent implements OnInit { |
|
||||
|
|
||||
ValueType = ValueType; |
|
||||
|
|
||||
ScadaSymbolPropertyType = ScadaSymbolPropertyType; |
|
||||
|
|
||||
scadaSymbolPropertyTypes = scadaSymbolPropertyTypes; |
|
||||
scadaSymbolPropertyTypeTranslations = scadaSymbolPropertyTypeTranslations; |
|
||||
|
|
||||
scadaSymbolPropertyRowClasses = scadaSymbolPropertyRowClasses; |
|
||||
|
|
||||
scadaSymbolPropertyFieldClasses = scadaSymbolPropertyFieldClasses; |
|
||||
|
|
||||
@Input() |
|
||||
isAdd = false; |
|
||||
|
|
||||
@Input() |
|
||||
property: ScadaSymbolProperty; |
|
||||
|
|
||||
@Input() |
|
||||
booleanPropertyIds: string[]; |
|
||||
|
|
||||
@Input() |
|
||||
disabled: boolean; |
|
||||
|
|
||||
@Input() |
|
||||
popover: TbPopoverComponent<ScadaSymbolPropertyPanelComponent>; |
|
||||
|
|
||||
@Output() |
|
||||
propertySettingsApplied = new EventEmitter<ScadaSymbolProperty>(); |
|
||||
|
|
||||
panelTitle: string; |
|
||||
|
|
||||
propertyFormGroup: UntypedFormGroup; |
|
||||
|
|
||||
private propertyType: ScadaSymbolPropertyType; |
|
||||
|
|
||||
constructor(private fb: UntypedFormBuilder, |
|
||||
private destroyRef: DestroyRef) { |
|
||||
} |
|
||||
|
|
||||
ngOnInit(): void { |
|
||||
this.panelTitle = this.isAdd ? 'scada.property.add-property' : 'scada.property.property-settings'; |
|
||||
this.propertyType = this.property.type; |
|
||||
this.propertyFormGroup = this.fb.group( |
|
||||
{ |
|
||||
id: [this.property.id, [Validators.required]], |
|
||||
name: [this.property.name, [Validators.required]], |
|
||||
type: [this.property.type, [Validators.required]], |
|
||||
default: [this.property.default, []], |
|
||||
required: [this.property.required, []], |
|
||||
subLabel: [this.property.subLabel, []], |
|
||||
divider: [this.property.divider, []], |
|
||||
fieldSuffix: [this.property.fieldSuffix, []], |
|
||||
disableOnProperty: [this.property.disableOnProperty, []], |
|
||||
rowClass: [(this.property.rowClass || '').split(' '), []], |
|
||||
fieldClass: [(this.property.fieldClass || '').split(' '), []], |
|
||||
min: [this.property.min, []], |
|
||||
max: [this.property.max, []], |
|
||||
step: [this.property.step, [Validators.min(0)]] |
|
||||
} |
|
||||
); |
|
||||
if (this.disabled) { |
|
||||
this.propertyFormGroup.disable({emitEvent: false}); |
|
||||
} else { |
|
||||
this.propertyFormGroup.get('type').valueChanges.pipe( |
|
||||
takeUntilDestroyed(this.destroyRef) |
|
||||
).subscribe(() => { |
|
||||
this.updateValidators(); |
|
||||
}); |
|
||||
this.updateValidators(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
cancel() { |
|
||||
this.popover?.hide(); |
|
||||
} |
|
||||
|
|
||||
applyPropertySettings() { |
|
||||
const property = this.propertyFormGroup.getRawValue(); |
|
||||
property.rowClass = (property.rowClass || []).join(' '); |
|
||||
property.fieldClass = (property.fieldClass || []).join(' '); |
|
||||
this.propertySettingsApplied.emit(property); |
|
||||
} |
|
||||
|
|
||||
private updateValidators() { |
|
||||
const type: ScadaSymbolPropertyType = this.propertyFormGroup.get('type').value; |
|
||||
if (type === ScadaSymbolPropertyType.number) { |
|
||||
this.propertyFormGroup.get('min').enable({emitEvent: false}); |
|
||||
this.propertyFormGroup.get('max').enable({emitEvent: false}); |
|
||||
this.propertyFormGroup.get('step').enable({emitEvent: false}); |
|
||||
} else { |
|
||||
this.propertyFormGroup.get('min').disable({emitEvent: false}); |
|
||||
this.propertyFormGroup.get('max').disable({emitEvent: false}); |
|
||||
this.propertyFormGroup.get('step').disable({emitEvent: false}); |
|
||||
} |
|
||||
if (this.propertyType !== type) { |
|
||||
const defaultValue = defaultPropertyValue(type); |
|
||||
this.propertyFormGroup.get('default').patchValue(defaultValue, {emitEvent: false}); |
|
||||
this.propertyType = type; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
///
|
|
||||
/// Copyright © 2016-2024 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 { JsonSettingsSchema } from '@shared/models/widget.models'; |
|
||||
|
|
||||
export interface JsonFormComponentData extends JsonSettingsSchema { |
|
||||
model?: any; |
|
||||
settingsDirective?: string; |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
<!-- |
|
||||
|
|
||||
Copyright © 2016-2024 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 [fullscreenElement]="reactFullscreen" |
|
||||
[fullscreen]="isFullscreen" |
|
||||
(fullscreenChanged)="onFullscreenChanged($event)"> |
|
||||
<div #reactRoot></div> |
|
||||
<div class="tb-json-form" #reactFullscreen></div> |
|
||||
</div> |
|
||||
@ -1,293 +0,0 @@ |
|||||
///
|
|
||||
/// Copyright © 2016-2024 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 { |
|
||||
ChangeDetectorRef, |
|
||||
Component, |
|
||||
ElementRef, |
|
||||
forwardRef, |
|
||||
Input, |
|
||||
OnChanges, |
|
||||
OnDestroy, |
|
||||
Renderer2, |
|
||||
SimpleChanges, |
|
||||
ViewChild, ViewContainerRef, |
|
||||
ViewEncapsulation |
|
||||
} from '@angular/core'; |
|
||||
import { ControlValueAccessor, UntypedFormControl, 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, unwrapModule } from '@app/core/utils'; |
|
||||
import { JsonFormProps } from './react/json-form.models'; |
|
||||
import inspector from 'schema-inspector'; |
|
||||
import tinycolor from 'tinycolor2'; |
|
||||
import { DialogService } from '@app/core/services/dialog.service'; |
|
||||
import JsonFormUtils from './react/json-form-utils'; |
|
||||
import { JsonFormComponentData } from './json-form-component.models'; |
|
||||
import { GroupInfo } from '@shared/models/widget.models'; |
|
||||
import { Observable } from 'rxjs/internal/Observable'; |
|
||||
import { forkJoin, from } from 'rxjs'; |
|
||||
import { TbPopoverService } from '@shared/components/popover.service'; |
|
||||
|
|
||||
@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 ControlValueAccessor, Validator, OnChanges, OnDestroy { |
|
||||
|
|
||||
@ViewChild('reactRoot', {static: true}) |
|
||||
reactRootElmRef: ElementRef<HTMLElement>; |
|
||||
|
|
||||
@ViewChild('reactFullscreen', {static: true}) |
|
||||
reactFullscreenElmRef: ElementRef<HTMLElement>; |
|
||||
|
|
||||
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), |
|
||||
onIconClick: this.onIconClick.bind(this), |
|
||||
onToggleFullscreen: this.onToggleFullscreen.bind(this), |
|
||||
onHelpClick: this.onHelpClick.bind(this) |
|
||||
}; |
|
||||
|
|
||||
data: JsonFormComponentData; |
|
||||
|
|
||||
model: any; |
|
||||
schema: any; |
|
||||
form: any; |
|
||||
groupInfoes: GroupInfo[]; |
|
||||
|
|
||||
isModelValid = true; |
|
||||
|
|
||||
isFullscreen = false; |
|
||||
fullscreenFinishFn: (el: Element) => void; |
|
||||
|
|
||||
private reactRoot: any; |
|
||||
|
|
||||
private propagateChange = null; |
|
||||
private propagateChangePending = false; |
|
||||
private writingValue = false; |
|
||||
private updateViewPending = false; |
|
||||
|
|
||||
constructor(public elementRef: ElementRef, |
|
||||
private dialogs: DialogService, |
|
||||
private popoverService: TbPopoverService, |
|
||||
private renderer: Renderer2, |
|
||||
private viewContainerRef: ViewContainerRef, |
|
||||
protected store: Store<AppState>, |
|
||||
private cd: ChangeDetectorRef) { |
|
||||
} |
|
||||
|
|
||||
ngOnDestroy(): void { |
|
||||
this.destroyReactSchemaForm(); |
|
||||
} |
|
||||
|
|
||||
registerOnChange(fn: any): void { |
|
||||
this.propagateChange = fn; |
|
||||
if (this.propagateChangePending) { |
|
||||
this.propagateChangePending = false; |
|
||||
setTimeout(() => { |
|
||||
this.propagateChange(this.data); |
|
||||
}, 0); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
registerOnTouched(fn: any): void { |
|
||||
} |
|
||||
|
|
||||
setDisabledState(isDisabled: boolean): void { |
|
||||
} |
|
||||
|
|
||||
public validate(c: UntypedFormControl) { |
|
||||
return this.isModelValid ? null : { |
|
||||
modelValid: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
writeValue(data: JsonFormComponentData): void { |
|
||||
this.writingValue = true; |
|
||||
this.data = data; |
|
||||
this.schema = this.data && this.data.schema ? deepClone(this.data.schema) : { |
|
||||
type: 'object' |
|
||||
}; |
|
||||
this.schema.strict = true; |
|
||||
this.form = this.data && this.data.form ? deepClone(this.data.form) : [ '*' ]; |
|
||||
this.groupInfoes = this.data && this.data.groupInfoes ? deepClone(this.data.groupInfoes) : []; |
|
||||
this.model = this.data && this.data.model || {}; |
|
||||
this.model = inspector.sanitize(this.schema, this.model).data; |
|
||||
this.updateAndRender(); |
|
||||
this.isModelValid = this.validateModel(); |
|
||||
this.writingValue = false; |
|
||||
if (!this.isModelValid || this.updateViewPending) { |
|
||||
this.updateView(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
updateView() { |
|
||||
if (!this.writingValue) { |
|
||||
this.updateViewPending = false; |
|
||||
if (this.data) { |
|
||||
this.data.model = this.model; |
|
||||
if (this.propagateChange) { |
|
||||
try { |
|
||||
this.propagateChange(this.data); |
|
||||
} catch (e) { |
|
||||
this.propagateChangePending = true; |
|
||||
} |
|
||||
} else { |
|
||||
this.propagateChangePending = true; |
|
||||
} |
|
||||
} |
|
||||
} else { |
|
||||
this.updateViewPending = true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
ngOnChanges(changes: SimpleChanges): void { |
|
||||
for (const propName of Object.keys(changes)) { |
|
||||
const change = changes[propName]; |
|
||||
if (!change.firstChange && change.currentValue !== change.previousValue) { |
|
||||
if (propName === 'readonly') { |
|
||||
this.updateAndRender(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private onModelChange(key: (string | number)[], val: any, forceUpdate = false) { |
|
||||
if (isString(val) && val === '') { |
|
||||
val = undefined; |
|
||||
} |
|
||||
if (JsonFormUtils.updateValue(key, this.model, val) || forceUpdate) { |
|
||||
this.isModelValid = this.validateModel(); |
|
||||
this.updateView(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private onColorClick(key: (string | number)[], |
|
||||
val: tinycolor.ColorFormats.RGBA, |
|
||||
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) { |
|
||||
this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((result) => { |
|
||||
if (!result?.canceled && colorSelectedFn) { |
|
||||
colorSelectedFn(tinycolor(result?.color).toRgb()); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private onIconClick(key: (string | number)[], |
|
||||
val: string, |
|
||||
iconSelectedFn: (icon: string) => void) { |
|
||||
this.dialogs.materialIconPicker(val).subscribe((result) => { |
|
||||
if (!result?.canceled && iconSelectedFn) { |
|
||||
iconSelectedFn(result?.icon); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
private onToggleFullscreen(fullscreenFinishFn?: (el: Element) => void) { |
|
||||
this.isFullscreen = !this.isFullscreen; |
|
||||
this.fullscreenFinishFn = fullscreenFinishFn; |
|
||||
this.cd.markForCheck(); |
|
||||
} |
|
||||
|
|
||||
onFullscreenChanged(fullscreen: boolean) { |
|
||||
this.formProps.isFullscreen = fullscreen; |
|
||||
this.renderReactSchemaForm(false); |
|
||||
if (this.fullscreenFinishFn) { |
|
||||
this.fullscreenFinishFn(this.reactFullscreenElmRef.nativeElement); |
|
||||
this.fullscreenFinishFn = null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) { |
|
||||
const trigger = event.currentTarget as Element; |
|
||||
this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef, helpId, '', '', null, helpVisibleFn, helpReadyFn); |
|
||||
} |
|
||||
|
|
||||
private updateAndRender() { |
|
||||
|
|
||||
this.formProps.option.formDefaults.readonly = this.readonly; |
|
||||
this.formProps.schema = this.schema; |
|
||||
this.formProps.form = this.form; |
|
||||
this.formProps.groupInfoes = this.groupInfoes; |
|
||||
this.formProps.model = this.model; |
|
||||
this.renderReactSchemaForm(); |
|
||||
} |
|
||||
|
|
||||
private renderReactSchemaForm(destroy: boolean = true) { |
|
||||
if (destroy) { |
|
||||
this.destroyReactSchemaForm(); |
|
||||
} |
|
||||
|
|
||||
// import ReactSchemaForm from './react/json-form-react';
|
|
||||
const reactSchemaFormObservables: Observable<any>[] = [ |
|
||||
from(import('react')), |
|
||||
from(import('react-dom')), |
|
||||
from(import('react-dom/client')), |
|
||||
from(import('./react/json-form-react')) |
|
||||
]; |
|
||||
forkJoin(reactSchemaFormObservables).subscribe( |
|
||||
(modules) => { |
|
||||
const react = unwrapModule(modules[0]); |
|
||||
const reactDomClient = unwrapModule(modules[2]); |
|
||||
const jsonFormReact = unwrapModule(modules[3]); |
|
||||
this.reactRoot = reactDomClient.createRoot(this.reactRootElmRef.nativeElement); |
|
||||
this.reactRoot.render(react.createElement(jsonFormReact, this.formProps)); |
|
||||
} |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
private destroyReactSchemaForm() { |
|
||||
this.reactRoot?.unmount(); |
|
||||
} |
|
||||
|
|
||||
private validateModel(): boolean { |
|
||||
if (this.schema && this.model) { |
|
||||
return JsonFormUtils.validateBySchema(this.schema, this.model).valid; |
|
||||
} |
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@ -1,239 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 * as ReactDOM from 'react-dom'; |
|
||||
import ThingsboardBaseComponent from './json-form-base-component'; |
|
||||
import reactCSS from 'reactcss'; |
|
||||
import Button from '@mui/material/Button'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import { IEditorProps } from 'react-ace/src/types'; |
|
||||
import { map, mergeMap } from 'rxjs/operators'; |
|
||||
import { getAce } from '@shared/models/ace/ace.models'; |
|
||||
import { from, lastValueFrom } from 'rxjs'; |
|
||||
import { Observable } from 'rxjs/internal/Observable'; |
|
||||
import { CircularProgress, IconButton } from '@mui/material'; |
|
||||
import { MouseEvent } from 'react'; |
|
||||
import { Help, HelpOutline } from '@mui/icons-material'; |
|
||||
import { unwrapModule } from '@core/utils'; |
|
||||
|
|
||||
const ReactAce = React.lazy(() => { |
|
||||
return lastValueFrom(getAce().pipe( |
|
||||
mergeMap(() => { |
|
||||
return from(import('react-ace')).pipe( |
|
||||
map((module) => unwrapModule(module) |
|
||||
)); |
|
||||
}) |
|
||||
)); |
|
||||
}); |
|
||||
|
|
||||
interface ThingsboardAceEditorProps extends JsonFormFieldProps { |
|
||||
mode: string; |
|
||||
onTidy: (value: string) => Observable<string>; |
|
||||
} |
|
||||
|
|
||||
interface ThingsboardAceEditorState extends JsonFormFieldState { |
|
||||
isFull: boolean; |
|
||||
fullscreenContainerElement: Element; |
|
||||
helpVisible: boolean; |
|
||||
helpReady: boolean; |
|
||||
focused: boolean; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardAceEditor extends React.Component<ThingsboardAceEditorProps, ThingsboardAceEditorState> { |
|
||||
|
|
||||
private aceEditor: IEditorProps; |
|
||||
|
|
||||
constructor(props: ThingsboardAceEditorProps) { |
|
||||
super(props); |
|
||||
this.onValueChanged = this.onValueChanged.bind(this); |
|
||||
this.onBlur = this.onBlur.bind(this); |
|
||||
this.onFocus = this.onFocus.bind(this); |
|
||||
this.onTidy = this.onTidy.bind(this); |
|
||||
this.onHelp = this.onHelp.bind(this); |
|
||||
this.onLoad = this.onLoad.bind(this); |
|
||||
this.onToggleFull = this.onToggleFull.bind(this); |
|
||||
const value = props.value ? props.value + '' : ''; |
|
||||
this.state = { |
|
||||
isFull: false, |
|
||||
fullscreenContainerElement: null, |
|
||||
helpVisible: false, |
|
||||
helpReady: true, |
|
||||
value, |
|
||||
focused: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
onValueChanged(value) { |
|
||||
this.setState({ |
|
||||
value |
|
||||
}); |
|
||||
this.props.onChangeValidate({ |
|
||||
target: { |
|
||||
value |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
onBlur() { |
|
||||
this.setState({ focused: false }); |
|
||||
} |
|
||||
|
|
||||
onFocus() { |
|
||||
this.setState({ focused: true }); |
|
||||
} |
|
||||
|
|
||||
onTidy() { |
|
||||
if (!this.props.form.readonly) { |
|
||||
const value = this.state.value; |
|
||||
this.props.onTidy(value).subscribe( |
|
||||
(processedValue) => { |
|
||||
this.setState({ |
|
||||
value: processedValue |
|
||||
}); |
|
||||
this.props.onChangeValidate({ |
|
||||
target: { |
|
||||
value: processedValue |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onHelp(event: MouseEvent) { |
|
||||
if (this.state.helpVisible && !this.state.helpReady) { |
|
||||
event.preventDefault(); |
|
||||
event.stopPropagation(); |
|
||||
} else { |
|
||||
this.props.onHelpClick(event, this.props.form.helpId, |
|
||||
(visible) => { |
|
||||
this.setState({ |
|
||||
helpVisible: visible |
|
||||
}); |
|
||||
}, (ready) => { |
|
||||
this.setState({ |
|
||||
helpReady: ready |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onLoad(editor: IEditorProps) { |
|
||||
this.aceEditor = editor; |
|
||||
} |
|
||||
|
|
||||
onToggleFull() { |
|
||||
this.props.onToggleFullscreen((el) => { |
|
||||
this.setState({ isFull: !this.state.isFull, fullscreenContainerElement: el }); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate() { |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
|
|
||||
const styles = reactCSS({ |
|
||||
default: { |
|
||||
tidyButtonStyle: { |
|
||||
color: '#7B7B7B', |
|
||||
minWidth: '32px', |
|
||||
minHeight: '15px', |
|
||||
lineHeight: '15px', |
|
||||
fontSize: '0.800rem', |
|
||||
margin: '0', |
|
||||
padding: '4px', |
|
||||
height: '23px', |
|
||||
borderRadius: '5px', |
|
||||
marginLeft: '5px' |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
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'; |
|
||||
} |
|
||||
let containerClass = 'tb-container'; |
|
||||
const style = this.props.form.style || {width: '100%'}; |
|
||||
if (this.state.isFull) { |
|
||||
containerClass += ' fullscreen-form-field'; |
|
||||
} |
|
||||
const formDom = ( |
|
||||
<div className={containerClass}> |
|
||||
<label className={labelClass}>{this.props.form.title}</label> |
|
||||
<div className='json-form-ace-editor'> |
|
||||
<div className='title-panel'> |
|
||||
<label>{this.props.mode}</label> |
|
||||
{ this.props.onTidy ? <Button style={ styles.tidyButtonStyle } |
|
||||
className='tidy-button' onClick={this.onTidy}>Tidy</Button> : null } |
|
||||
{ this.props.form.helpId ? <div style={ {position: 'relative', display: 'inline-block', marginLeft: '5px'} }> |
|
||||
<IconButton color='primary' |
|
||||
className='help-button' onClick={this.onHelp}> |
|
||||
{this.state.helpVisible ? <Help /> : <HelpOutline /> } |
|
||||
</IconButton> |
|
||||
{ this.state.helpVisible && !this.state.helpReady ? |
|
||||
<div className='tb-absolute-fill help-button-loading'> |
|
||||
<CircularProgress size={18} thickness={4}/> |
|
||||
</div> : null }</div> : null } |
|
||||
<Button style={ styles.tidyButtonStyle } |
|
||||
className='tidy-button' onClick={this.onToggleFull}> |
|
||||
{this.state.isFull ? |
|
||||
'Exit fullscreen' : 'Fullscreen'} |
|
||||
</Button> |
|
||||
</div> |
|
||||
<React.Suspense fallback={<div>Loading...</div>}> |
|
||||
<ReactAce mode={this.props.mode} |
|
||||
theme={'textmate'} |
|
||||
height={this.state.isFull ? '100%' : '150px'} |
|
||||
width={this.state.isFull ? '100%' : '300px'} |
|
||||
onChange={this.onValueChanged} |
|
||||
onFocus={this.onFocus} |
|
||||
onBlur={this.onBlur} |
|
||||
onLoad={this.onLoad} |
|
||||
name={this.props.form.title} |
|
||||
value={this.state.value} |
|
||||
readOnly={this.props.form.readonly} |
|
||||
editorProps={{$blockScrolling: Infinity}} |
|
||||
enableBasicAutocompletion={true} |
|
||||
enableSnippets={true} |
|
||||
enableLiveAutocompletion={true} |
|
||||
style={style}/> |
|
||||
</React.Suspense> |
|
||||
</div> |
|
||||
<div className='json-form-error' |
|
||||
style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}</div> |
|
||||
</div> |
|
||||
); |
|
||||
if (this.state.isFull) { |
|
||||
return ReactDOM.createPortal(formDom, this.state.fullscreenContainerElement); |
|
||||
} else { |
|
||||
return ( |
|
||||
<div> |
|
||||
{formDom} |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardBaseComponent(ThingsboardAceEditor); |
|
||||
@ -1,177 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 ThingsboardBaseComponent from './json-form-base-component'; |
|
||||
import Button from '@mui/material/Button'; |
|
||||
import _ from 'lodash'; |
|
||||
import IconButton from '@mui/material/IconButton'; |
|
||||
import Clear from '@mui/icons-material/Clear'; |
|
||||
import Add from '@mui/icons-material/Add'; |
|
||||
import Tooltip from '@mui/material/Tooltip'; |
|
||||
import { |
|
||||
JsonFormData, |
|
||||
JsonFormFieldProps, |
|
||||
JsonFormFieldState |
|
||||
} from '@shared/components/json-form/react/json-form.models'; |
|
||||
|
|
||||
interface ThingsboardArrayState extends JsonFormFieldState { |
|
||||
model: any[]; |
|
||||
keys: number[]; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardArray extends React.Component<JsonFormFieldProps, ThingsboardArrayState> { |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
super(props); |
|
||||
this.onAppend = this.onAppend.bind(this); |
|
||||
this.onDelete = this.onDelete.bind(this); |
|
||||
const model = JsonFormUtils.selectOrSet(this.props.form.key, this.props.model) || []; |
|
||||
const keys: number[] = []; |
|
||||
for (let i = 0; i < model.length; i++) { |
|
||||
keys.push(i); |
|
||||
} |
|
||||
this.state = { |
|
||||
model, |
|
||||
keys |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
componentDidMount() { |
|
||||
if (this.props.form.startEmpty !== true && this.state.model.length === 0) { |
|
||||
this.onAppend(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onAppend() { |
|
||||
let empty; |
|
||||
if (this.props.form && this.props.form.schema && this.props.form.schema.items) { |
|
||||
const items = this.props.form.schema.items; |
|
||||
if (items.type && items.type.indexOf('object') !== -1) { |
|
||||
empty = {}; |
|
||||
if (!this.props.options || this.props.options.setSchemaDefaults !== false) { |
|
||||
empty = typeof items.default !== 'undefined' ? items.default : empty; |
|
||||
if (empty) { |
|
||||
JsonFormUtils.traverseSchema(items, (prop, path) => { |
|
||||
if (typeof prop.default !== 'undefined') { |
|
||||
JsonFormUtils.selectOrSet(path, empty, prop.default); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} else if (items.type && items.type.indexOf('array') !== -1) { |
|
||||
empty = []; |
|
||||
if (!this.props.options || this.props.options.setSchemaDefaults !== false) { |
|
||||
empty = items.default || empty; |
|
||||
} |
|
||||
} else { |
|
||||
if (!this.props.options || this.props.options.setSchemaDefaults !== false) { |
|
||||
empty = items.default || empty; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
const newModel = this.state.model; |
|
||||
newModel.push(empty); |
|
||||
const newKeys = this.state.keys; |
|
||||
let key = 0; |
|
||||
if (newKeys.length > 0) { |
|
||||
key = newKeys[newKeys.length - 1] + 1; |
|
||||
} |
|
||||
newKeys.push(key); |
|
||||
this.setState({ |
|
||||
model: newModel, |
|
||||
keys: newKeys |
|
||||
} |
|
||||
); |
|
||||
this.props.onChangeValidate(this.state.model, true); |
|
||||
} |
|
||||
|
|
||||
onDelete(index: number) { |
|
||||
const newModel = this.state.model; |
|
||||
newModel.splice(index, 1); |
|
||||
const newKeys = this.state.keys; |
|
||||
newKeys.splice(index, 1); |
|
||||
this.setState( |
|
||||
{ |
|
||||
model: newModel, |
|
||||
keys: newKeys |
|
||||
} |
|
||||
); |
|
||||
this.props.onChangeValidate(this.state.model, true); |
|
||||
} |
|
||||
|
|
||||
setIndex(index: number) { |
|
||||
return (form: JsonFormData) => { |
|
||||
if (form.key) { |
|
||||
form.key[form.key.indexOf('')] = index; |
|
||||
} |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
copyWithIndex(form: JsonFormData, index: number): JsonFormData { |
|
||||
const copy: JsonFormData = _.cloneDeep(form); |
|
||||
copy.arrayIndex = index; |
|
||||
JsonFormUtils.traverseForm(copy, this.setIndex(index)); |
|
||||
return copy; |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const arrays = []; |
|
||||
const model = this.state.model; |
|
||||
const keys = this.state.keys; |
|
||||
for (let i = 0; i < model.length; i++ ) { |
|
||||
let removeButton: React.JSX.Element = null; |
|
||||
if (!this.props.form.readonly) { |
|
||||
const boundOnDelete = this.onDelete.bind(this, i); |
|
||||
removeButton = <Tooltip title='Remove'><IconButton onClick={boundOnDelete}><Clear/></IconButton></Tooltip>; |
|
||||
} |
|
||||
const forms = (this.props.form.items as JsonFormData[]).map((form, index) => { |
|
||||
const copy = this.copyWithIndex(form, i); |
|
||||
return this.props.builder(copy, this.props.model, index, this.props.onChange, |
|
||||
this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, |
|
||||
this.props.onHelpClick, this.props.mapper); |
|
||||
}); |
|
||||
arrays.push( |
|
||||
<li key={keys[i]} className='list-group-item'> |
|
||||
{removeButton} |
|
||||
{forms} |
|
||||
</li> |
|
||||
); |
|
||||
} |
|
||||
let addButton: JSX.Element = null; |
|
||||
if (!this.props.form.readonly) { |
|
||||
addButton = <Button variant='contained' |
|
||||
color='primary' |
|
||||
startIcon={<Add/>} |
|
||||
style={{marginBottom: '8px'}} |
|
||||
onClick={this.onAppend}>{this.props.form.add || 'New'}</Button>; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div> |
|
||||
<div className='tb-container'> |
|
||||
<div className='tb-head-label'>{this.props.form.title}</div> |
|
||||
<ol className='list-group'> |
|
||||
{arrays} |
|
||||
</ol> |
|
||||
</div> |
|
||||
{addButton} |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardBaseComponent(ThingsboardArray); |
|
||||
@ -1,122 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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'; |
|
||||
import { isDefinedAndNotNull } from '@core/utils'; |
|
||||
|
|
||||
export default ThingsboardBaseComponent => class<P extends JsonFormFieldProps> |
|
||||
extends React.Component<P, JsonFormFieldState> { |
|
||||
|
|
||||
constructor(props: P) { |
|
||||
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, forceUpdate?: boolean) { |
|
||||
let value = null; |
|
||||
if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') { |
|
||||
if (!e || e.target?.value === null || e.target?.value === '') { |
|
||||
value = undefined; |
|
||||
} else if (typeof e === 'number') { |
|
||||
value = Number(e); |
|
||||
} 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, forceUpdate); |
|
||||
} |
|
||||
|
|
||||
defaultValue() { |
|
||||
let value = JsonFormUtils.selectOrSet(this.props.form.key, this.props.model); |
|
||||
if (this.props.form.schema.type === 'boolean') { |
|
||||
if (typeof value !== 'boolean' && typeof this.props.form.default === 'boolean') { |
|
||||
value = this.props.form.default; |
|
||||
} |
|
||||
if (typeof value !== 'boolean' && this.props.form.schema && typeof this.props.form.schema.default === 'boolean') { |
|
||||
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' && typeof this.props.form.default === 'number') { |
|
||||
value = this.props.form.default; |
|
||||
} |
|
||||
if (typeof value !== 'number' && this.props.form.schema && typeof this.props.form.schema.default === 'number') { |
|
||||
value = this.props.form.schema.default; |
|
||||
} |
|
||||
if (typeof value !== 'number' && this.props.form.titleMap && typeof this.props.form.titleMap[0].value === 'number') { |
|
||||
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 && isDefinedAndNotNull(this.props.form.default)) { |
|
||||
value = this.props.form.default; |
|
||||
} |
|
||||
if (!value && this.props.form.schema && isDefinedAndNotNull(this.props.form.schema.default)) { |
|
||||
value = this.props.form.schema.default; |
|
||||
} |
|
||||
if (!value && this.props.form.titleMap && isDefinedAndNotNull(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>; |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
@ -1,46 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 '@mui/material/Checkbox'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from './json-form.models.js'; |
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'; |
|
||||
|
|
||||
class ThingsboardCheckbox extends React.Component<JsonFormFieldProps, JsonFormFieldState> { |
|
||||
render() { |
|
||||
return ( |
|
||||
<div> |
|
||||
<FormControlLabel |
|
||||
control={ |
|
||||
<Checkbox |
|
||||
color={'secondary'} |
|
||||
name={this.props.form.key.slice(-1)[0] + ''} |
|
||||
value={this.props.form.key.slice(-1)[0]} |
|
||||
checked={this.props.value || false} |
|
||||
disabled={this.props.form.readonly} |
|
||||
onChange={(e) => { |
|
||||
this.props.onChangeValidate(e); |
|
||||
}} |
|
||||
/> |
|
||||
} |
|
||||
label={this.props.form.title} |
|
||||
/> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardBaseComponent(ThingsboardCheckbox); |
|
||||
@ -1,186 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 reactCSS from 'reactcss'; |
|
||||
import tinycolor from 'tinycolor2'; |
|
||||
import TextField from '@mui/material/TextField'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import IconButton from '@mui/material/IconButton'; |
|
||||
import Clear from '@mui/icons-material/Clear'; |
|
||||
import Tooltip from '@mui/material/Tooltip'; |
|
||||
|
|
||||
interface ThingsboardColorState extends JsonFormFieldState { |
|
||||
color: tinycolor.ColorFormats.RGBA | null; |
|
||||
focused: boolean; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardColor extends React.Component<JsonFormFieldProps, ThingsboardColorState> { |
|
||||
|
|
||||
containerRef = React.createRef<HTMLDivElement>(); |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
super(props); |
|
||||
this.onBlur = this.onBlur.bind(this); |
|
||||
this.onFocus = this.onFocus.bind(this); |
|
||||
this.onValueChanged = this.onValueChanged.bind(this); |
|
||||
this.onSwatchClick = this.onSwatchClick.bind(this); |
|
||||
this.onClear = this.onClear.bind(this); |
|
||||
const value = props.value ? props.value + '' : null; |
|
||||
const color = value != null ? tinycolor(value).toRgb() : null; |
|
||||
this.state = { |
|
||||
color, |
|
||||
focused: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
onBlur() { |
|
||||
this.setState({focused: false}); |
|
||||
} |
|
||||
|
|
||||
onFocus() { |
|
||||
this.setState({focused: true}); |
|
||||
} |
|
||||
|
|
||||
componentDidMount() { |
|
||||
const node = this.containerRef.current; |
|
||||
const colContainer = $(node).children('#color-container'); |
|
||||
colContainer.click(() => { |
|
||||
if (!this.props.form.readonly) { |
|
||||
this.onSwatchClick(); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
componentWillUnmount() { |
|
||||
const node = this.containerRef.current; |
|
||||
const colContainer = $(node).children('#color-container'); |
|
||||
colContainer.off( 'click' ); |
|
||||
} |
|
||||
|
|
||||
onValueChanged(value: tinycolor.ColorFormats.RGBA | null) { |
|
||||
let color: tinycolor.Instance = null; |
|
||||
if (value != null) { |
|
||||
color = tinycolor(value); |
|
||||
} |
|
||||
this.setState({ |
|
||||
color: value |
|
||||
}); |
|
||||
let colorValue = ''; |
|
||||
if (color != null && color.getAlpha() !== 1) { |
|
||||
colorValue = color.toRgbString(); |
|
||||
} else if (color != null) { |
|
||||
colorValue = color.toHexString(); |
|
||||
} |
|
||||
this.props.onChangeValidate({ |
|
||||
target: { |
|
||||
value: colorValue |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
onSwatchClick() { |
|
||||
this.props.onColorClick(this.props.form.key, this.state.color, |
|
||||
(color) => { |
|
||||
this.onValueChanged(color); |
|
||||
} |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
onClear(event: React.MouseEvent) { |
|
||||
if (event) { |
|
||||
event.stopPropagation(); |
|
||||
} |
|
||||
this.onValueChanged(null); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
|
|
||||
let background = 'rgba(0,0,0,0)'; |
|
||||
if (this.state.color != null) { |
|
||||
background = `rgba(${ this.state.color.r }, ${ this.state.color.g }, ${ this.state.color.b }, ${ this.state.color.a })`; |
|
||||
} |
|
||||
|
|
||||
const styles = reactCSS({ |
|
||||
default: { |
|
||||
color: { |
|
||||
background: `${ background }` |
|
||||
}, |
|
||||
swatch: { |
|
||||
display: 'inline-block', |
|
||||
marginRight: '10px', |
|
||||
marginTop: 'auto', |
|
||||
marginBottom: 'auto', |
|
||||
cursor: 'pointer', |
|
||||
opacity: `${ this.props.form.readonly ? '0.6' : '1' }` |
|
||||
}, |
|
||||
swatchText: { |
|
||||
width: '100%' |
|
||||
}, |
|
||||
container: { |
|
||||
display: 'flex', |
|
||||
flexDirection: 'row', |
|
||||
alignItems: 'center' |
|
||||
}, |
|
||||
colorContainer: { |
|
||||
display: 'flex', |
|
||||
width: '100%' |
|
||||
} |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
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 stringColor = ''; |
|
||||
if (this.state.color != null) { |
|
||||
const color = tinycolor(this.state.color); |
|
||||
stringColor = color.toRgbString(); |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div ref={this.containerRef} style={ styles.container }> |
|
||||
<div id='color-container' style={ styles.colorContainer }> |
|
||||
<div className='tb-color-preview' style={ styles.swatch }> |
|
||||
<div className='tb-color-result' style={ styles.color }/> |
|
||||
</div> |
|
||||
<TextField |
|
||||
variant={'standard'} |
|
||||
className={fieldClass} |
|
||||
label={this.props.form.title} |
|
||||
error={!this.props.valid} |
|
||||
helperText={this.props.valid ? this.props.form.placeholder : this.props.error} |
|
||||
value={stringColor} |
|
||||
disabled={this.props.form.readonly} |
|
||||
onFocus={this.onFocus} |
|
||||
onBlur={this.onBlur} |
|
||||
style={ styles.swatchText }/> |
|
||||
</div> |
|
||||
<Tooltip title='Clear' placement='top'><IconButton onClick={this.onClear}><Clear/></IconButton></Tooltip> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardBaseComponent(ThingsboardColor); |
|
||||
@ -1,40 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 ThingsboardAceEditor from './json-form-ace-editor'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import { Observable } from 'rxjs/internal/Observable'; |
|
||||
import { beautifyCss } from '@shared/models/beautify.models'; |
|
||||
|
|
||||
class ThingsboardCss extends React.Component<JsonFormFieldProps, JsonFormFieldState> { |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
super(props); |
|
||||
this.onTidyCss = this.onTidyCss.bind(this); |
|
||||
} |
|
||||
|
|
||||
onTidyCss(css: string): Observable<string> { |
|
||||
return beautifyCss(css, {indent_size: 4}); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return ( |
|
||||
<ThingsboardAceEditor {...this.props} mode='css' onTidy={this.onTidyCss} {...this.state}></ThingsboardAceEditor> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardCss; |
|
||||
@ -1,83 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment' |
|
||||
import { LocalizationProvider, DatePicker } from '@mui/x-date-pickers'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import moment from 'moment'; |
|
||||
|
|
||||
interface ThingsboardDateState extends JsonFormFieldState { |
|
||||
currentValue: Date | null; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardDate extends React.Component<JsonFormFieldProps, ThingsboardDateState> { |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
super(props); |
|
||||
this.onDatePicked = this.onDatePicked.bind(this); |
|
||||
let value: Date | null = null; |
|
||||
if (this.props.value && typeof this.props.value === 'number') { |
|
||||
value = new Date(this.props.value); |
|
||||
} |
|
||||
this.state = { |
|
||||
currentValue: value |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
|
|
||||
onDatePicked(date: moment.Moment | null) { |
|
||||
this.setState({ |
|
||||
currentValue: date?.toDate() |
|
||||
}); |
|
||||
this.props.onChangeValidate(date ? date.valueOf() : null); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
|
|
||||
let fieldClass = 'tb-date-field'; |
|
||||
if (this.props.form.required) { |
|
||||
fieldClass += ' tb-required'; |
|
||||
} |
|
||||
if (this.props.form.readonly) { |
|
||||
fieldClass += ' tb-readonly'; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<LocalizationProvider dateAdapter={AdapterMoment}> |
|
||||
<div style={{width: '100%', display: 'block'}}> |
|
||||
<DatePicker |
|
||||
slotProps={{ |
|
||||
textField: { |
|
||||
variant: 'standard' |
|
||||
} |
|
||||
}} |
|
||||
format='MM/DD/YYYY' |
|
||||
className={fieldClass} |
|
||||
label={this.props.form.title} |
|
||||
value={moment(this.state.currentValue?.valueOf())} |
|
||||
onChange={this.onDatePicked} |
|
||||
disabled={this.props.form.readonly} |
|
||||
sx={this.props.form.style || {width: '100%'}} |
|
||||
/> |
|
||||
|
|
||||
</div> |
|
||||
</LocalizationProvider> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardBaseComponent(ThingsboardDate); |
|
||||
@ -1,44 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 { |
|
||||
JsonFormData, |
|
||||
JsonFormFieldProps, |
|
||||
JsonFormFieldState |
|
||||
} from '@shared/components/json-form/react/json-form.models'; |
|
||||
|
|
||||
class ThingsboardFieldSet extends React.Component<JsonFormFieldProps, JsonFormFieldState> { |
|
||||
|
|
||||
render() { |
|
||||
const forms = (this.props.form.items as JsonFormData[]).map((form: JsonFormData, index) => { |
|
||||
return this.props.builder(form, this.props.model, index, this.props.onChange, |
|
||||
this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.onHelpClick, this.props.mapper); |
|
||||
}); |
|
||||
|
|
||||
return ( |
|
||||
<div style={{paddingTop: '20px'}}> |
|
||||
<div className='tb-head-label'> |
|
||||
{this.props.form.title} |
|
||||
</div> |
|
||||
<div> |
|
||||
{forms} |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardFieldSet; |
|
||||
@ -1,40 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 ThingsboardAceEditor from './json-form-ace-editor'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import { Observable } from 'rxjs/internal/Observable'; |
|
||||
import { beautifyHtml } from '@shared/models/beautify.models'; |
|
||||
|
|
||||
class ThingsboardHtml extends React.Component<JsonFormFieldProps, JsonFormFieldState> { |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
super(props); |
|
||||
this.onTidyHtml = this.onTidyHtml.bind(this); |
|
||||
} |
|
||||
|
|
||||
onTidyHtml(html: string): Observable<string> { |
|
||||
return beautifyHtml(html, {indent_size: 4}); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return ( |
|
||||
<ThingsboardAceEditor {...this.props} mode='html' onTidy={this.onTidyHtml} {...this.state}></ThingsboardAceEditor> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardHtml; |
|
||||
@ -1,162 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 { MouseEvent } from 'react'; |
|
||||
import ThingsboardBaseComponent from './json-form-base-component'; |
|
||||
import reactCSS from 'reactcss'; |
|
||||
import TextField from '@mui/material/TextField'; |
|
||||
import IconButton from '@mui/material/IconButton'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import Clear from '@mui/icons-material/Clear'; |
|
||||
import Icon from '@mui/material/Icon'; |
|
||||
import Tooltip from '@mui/material/Tooltip'; |
|
||||
|
|
||||
interface ThingsboardIconState extends JsonFormFieldState { |
|
||||
icon: string | null; |
|
||||
focused: boolean; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardIcon extends React.Component<JsonFormFieldProps, ThingsboardIconState> { |
|
||||
|
|
||||
containerRef = React.createRef<HTMLDivElement>(); |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
super(props); |
|
||||
this.onBlur = this.onBlur.bind(this); |
|
||||
this.onFocus = this.onFocus.bind(this); |
|
||||
this.onValueChanged = this.onValueChanged.bind(this); |
|
||||
this.onIconClick = this.onIconClick.bind(this); |
|
||||
this.onClear = this.onClear.bind(this); |
|
||||
const icon = props.value ? props.value : ''; |
|
||||
this.state = { |
|
||||
icon, |
|
||||
focused: false |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
onBlur() { |
|
||||
this.setState({focused: false}); |
|
||||
} |
|
||||
|
|
||||
onFocus() { |
|
||||
this.setState({focused: true}); |
|
||||
} |
|
||||
|
|
||||
componentDidMount() { |
|
||||
const node = this.containerRef.current; |
|
||||
const iconContainer = $(node).children('#icon-container'); |
|
||||
iconContainer.on('click', (event) => { |
|
||||
if (!this.props.form.readonly) { |
|
||||
this.onIconClick(event); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
componentWillUnmount() { |
|
||||
const node = this.containerRef.current; |
|
||||
const iconContainer = $(node).children('#icon-container'); |
|
||||
iconContainer.off( 'click' ); |
|
||||
} |
|
||||
|
|
||||
onValueChanged(value: string | null) { |
|
||||
this.setState({ |
|
||||
icon: value |
|
||||
}); |
|
||||
this.props.onChange(this.props.form.key, value); |
|
||||
} |
|
||||
|
|
||||
onIconClick(_event) { |
|
||||
this.props.onIconClick(this.props.form.key, this.state.icon, |
|
||||
(color) => { |
|
||||
this.onValueChanged(color); |
|
||||
} |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
onClear(event: MouseEvent) { |
|
||||
if (event) { |
|
||||
event.stopPropagation(); |
|
||||
} |
|
||||
this.onValueChanged(''); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
|
|
||||
const styles = reactCSS({ |
|
||||
default: { |
|
||||
container: { |
|
||||
display: 'flex', |
|
||||
flexDirection: 'row', |
|
||||
alignItems: 'center' |
|
||||
}, |
|
||||
icon: { |
|
||||
padding: '12px', |
|
||||
marginRight: '10px', |
|
||||
marginBottom: 'auto', |
|
||||
cursor: 'pointer', |
|
||||
border: 'solid 1px rgba(0, 0, 0, .27)', |
|
||||
borderRadius: '0' |
|
||||
}, |
|
||||
iconContainer: { |
|
||||
display: 'flex', |
|
||||
width: '100%' |
|
||||
}, |
|
||||
iconText: { |
|
||||
width: '100%' |
|
||||
}, |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
let fieldClass = 'tb-field'; |
|
||||
if (this.props.form.required) { |
|
||||
fieldClass += ' tb-required'; |
|
||||
} |
|
||||
if (this.state.focused) { |
|
||||
fieldClass += ' tb-focused'; |
|
||||
} |
|
||||
|
|
||||
let pickedIcon = 'more_horiz'; |
|
||||
let icon = ''; |
|
||||
if (this.state.icon !== '') { |
|
||||
pickedIcon = this.state.icon; |
|
||||
icon = this.state.icon; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div ref={this.containerRef} style={ styles.container }> |
|
||||
<div id='icon-container' style={ styles.iconContainer }> |
|
||||
<IconButton style={ styles.icon }> |
|
||||
<Icon>{pickedIcon}</Icon> |
|
||||
</IconButton> |
|
||||
<TextField |
|
||||
variant={'standard'} |
|
||||
className={fieldClass} |
|
||||
label={this.props.form.title} |
|
||||
error={!this.props.valid} |
|
||||
helperText={this.props.valid ? this.props.form.placeholder : this.props.error} |
|
||||
value={icon} |
|
||||
disabled={this.props.form.readonly} |
|
||||
onFocus={this.onFocus} |
|
||||
onBlur={this.onBlur} |
|
||||
style={ styles.iconText } /> |
|
||||
</div> |
|
||||
<Tooltip title='Clear' placement='top'><IconButton onClick={this.onClear}><Clear/></IconButton></Tooltip> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardBaseComponent(ThingsboardIcon); |
|
||||
@ -1,108 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 React, { MouseEvent } from 'react'; |
|
||||
import Dropzone from 'react-dropzone'; |
|
||||
import ThingsboardBaseComponent from './json-form-base-component'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import IconButton from '@mui/material/IconButton'; |
|
||||
import Clear from '@mui/icons-material/Clear'; |
|
||||
import Tooltip from '@mui/material/Tooltip'; |
|
||||
|
|
||||
interface ThingsboardImageState extends JsonFormFieldState { |
|
||||
imageUrl: string; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardImage extends React.Component<JsonFormFieldProps, ThingsboardImageState> { |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
super(props); |
|
||||
this.onDrop = this.onDrop.bind(this); |
|
||||
this.onClear = this.onClear.bind(this); |
|
||||
const value = props.value ? props.value + '' : null; |
|
||||
this.state = { |
|
||||
imageUrl: value |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
onDrop(acceptedFiles: File[]) { |
|
||||
const reader = new FileReader(); |
|
||||
reader.onload = () => { |
|
||||
this.onValueChanged(reader.result as string); |
|
||||
}; |
|
||||
reader.readAsDataURL(acceptedFiles[0]); |
|
||||
} |
|
||||
|
|
||||
onValueChanged(value: string) { |
|
||||
this.setState({ |
|
||||
imageUrl: value |
|
||||
}); |
|
||||
this.props.onChangeValidate({ |
|
||||
target: { |
|
||||
value |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
onClear(event: MouseEvent) { |
|
||||
if (event) { |
|
||||
event.stopPropagation(); |
|
||||
} |
|
||||
this.onValueChanged(''); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
|
|
||||
let labelClass = 'tb-label'; |
|
||||
if (this.props.form.required) { |
|
||||
labelClass += ' tb-required'; |
|
||||
} |
|
||||
if (this.props.form.readonly) { |
|
||||
labelClass += ' tb-readonly'; |
|
||||
} |
|
||||
|
|
||||
let previewComponent: React.JSX.Element; |
|
||||
if (this.state.imageUrl) { |
|
||||
previewComponent = <img className='tb-image-preview' src={this.state.imageUrl} />; |
|
||||
} else { |
|
||||
previewComponent = <div>No image selected</div>; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div className='tb-container'> |
|
||||
<label className={labelClass}>{this.props.form.title}</label> |
|
||||
<div className='tb-image-select-container'> |
|
||||
<div className='tb-image-preview-container'>{previewComponent}</div> |
|
||||
<div className='tb-image-clear-container'> |
|
||||
<Tooltip title='Clear' placement='top'> |
|
||||
<IconButton className='tb-image-clear-btn' onClick={this.onClear}><Clear/></IconButton> |
|
||||
</Tooltip> |
|
||||
</div> |
|
||||
<Dropzone onDrop={this.onDrop} |
|
||||
accept={ {'image/*': []} } multiple={false}> |
|
||||
{({getRootProps, getInputProps}) => ( |
|
||||
<div className='tb-dropzone' {...getRootProps()}> |
|
||||
<div>Drop an image or click to select a file to upload.</div> |
|
||||
<input {...getInputProps()} /> |
|
||||
</div> |
|
||||
)} |
|
||||
</Dropzone> |
|
||||
</div> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardBaseComponent(ThingsboardImage); |
|
||||
@ -1,40 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 ThingsboardAceEditor from './json-form-ace-editor'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import { Observable } from 'rxjs/internal/Observable'; |
|
||||
import { beautifyJs } from '@shared/models/beautify.models'; |
|
||||
|
|
||||
class ThingsboardJavaScript extends React.Component<JsonFormFieldProps, JsonFormFieldState> { |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
super(props); |
|
||||
this.onTidyJavascript = this.onTidyJavascript.bind(this); |
|
||||
} |
|
||||
|
|
||||
onTidyJavascript(javascript: string): Observable<string> { |
|
||||
return beautifyJs(javascript, {indent_size: 4, wrap_line_length: 60}); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return ( |
|
||||
<ThingsboardAceEditor {...this.props} mode='javascript' onTidy={this.onTidyJavascript} {...this.state}></ThingsboardAceEditor> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardJavaScript; |
|
||||
@ -1,40 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 ThingsboardAceEditor from './json-form-ace-editor'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import { Observable } from 'rxjs/internal/Observable'; |
|
||||
import { beautifyJs } from '@shared/models/beautify.models'; |
|
||||
|
|
||||
class ThingsboardJson extends React.Component<JsonFormFieldProps, JsonFormFieldState> { |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
super(props); |
|
||||
this.onTidyJson = this.onTidyJson.bind(this); |
|
||||
} |
|
||||
|
|
||||
onTidyJson(json: string): Observable<string> { |
|
||||
return beautifyJs(json, {indent_size: 4}); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
return ( |
|
||||
<ThingsboardAceEditor {...this.props} mode='json' onTidy={this.onTidyJson} {...this.state}></ThingsboardAceEditor> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardJson; |
|
||||
@ -1,99 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 '@mui/material'; |
|
||||
import { ChangeEvent } from 'react'; |
|
||||
|
|
||||
interface ThingsboardNumberState extends JsonFormFieldState { |
|
||||
focused: boolean; |
|
||||
lastSuccessfulValue: number; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardNumber extends React.Component<JsonFormFieldProps, ThingsboardNumberState> { |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
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: any) { |
|
||||
return n === null || n === '' || !isNaN(n) && isFinite(n); |
|
||||
} |
|
||||
|
|
||||
onBlur() { |
|
||||
this.setState({focused: false}); |
|
||||
} |
|
||||
|
|
||||
onFocus() { |
|
||||
this.setState({focused: true}); |
|
||||
} |
|
||||
|
|
||||
preValidationCheck(e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) { |
|
||||
if (this.isNumeric(e.target.value)) { |
|
||||
this.setState({ |
|
||||
lastSuccessfulValue: Number(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 |
|
||||
variant={'standard'} |
|
||||
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); |
|
||||
@ -1,51 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'; |
|
||||
import { FormLabel, Radio, RadioGroup } from '@mui/material'; |
|
||||
import FormControl from '@mui/material/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 color={'secondary'} />} label={item.name} key={index} /> |
|
||||
); |
|
||||
}); |
|
||||
|
|
||||
let row = false; |
|
||||
if (this.props.form.direction === 'row') { |
|
||||
row = true; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<FormControl component='fieldset' |
|
||||
className={this.props.form.htmlClass} |
|
||||
disabled={this.props.form.readonly}> |
|
||||
<FormLabel component='legend'>{this.props.form.title}</FormLabel> |
|
||||
<RadioGroup row={row} name={this.props.form.title} value={this.props.value} onChange={(e) => { |
|
||||
this.props.onChangeValidate(e); |
|
||||
}}> |
|
||||
{items} |
|
||||
</RadioGroup> |
|
||||
</FormControl> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardBaseComponent(ThingsboardRadios); |
|
||||
@ -1,202 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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'; |
|
||||
import { |
|
||||
JsonFormFieldProps, |
|
||||
JsonFormFieldState, |
|
||||
KeyLabelItem |
|
||||
} from '@shared/components/json-form/react/json-form.models'; |
|
||||
import { Mode } from 'rc-select/lib/interface'; |
|
||||
import { deepClone } from '@core/utils'; |
|
||||
|
|
||||
interface ThingsboardRcSelectState extends JsonFormFieldState { |
|
||||
currentValue: KeyLabelItem | KeyLabelItem[]; |
|
||||
items: Array<KeyLabelItem>; |
|
||||
focused: boolean; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, ThingsboardRcSelectState> { |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
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 as KeyLabelItem[], |
|
||||
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) { |
|
||||
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) { |
|
||||
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: React.JSX.Element[] = []; |
|
||||
if (this.state.items && this.state.items.length > 0) { |
|
||||
options = this.state.items.map((item) => ( |
|
||||
<Option key={item.value} 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'; |
|
||||
} |
|
||||
let mode: Mode; |
|
||||
let value = this.state.currentValue; |
|
||||
if (this.props.form.tags || this.props.form.multiple) { |
|
||||
value = deepClone(value); |
|
||||
if (this.props.form.tags) { |
|
||||
mode = 'tags'; |
|
||||
} else if (this.props.form.multiple) { |
|
||||
mode = 'multiple'; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
const dropdownStyle = {...this.props.form.dropdownStyle, ...{zIndex: 100001}}; |
|
||||
let dropdownClassName = 'tb-rc-select-dropdown'; |
|
||||
if (this.props.form.dropdownClassName) { |
|
||||
dropdownClassName += ' ' + this.props.form.dropdownClassName; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div className='tb-container'> |
|
||||
<label className={labelClass}>{this.props.form.title}</label> |
|
||||
<Select |
|
||||
className={this.props.form.className} |
|
||||
dropdownClassName={dropdownClassName} |
|
||||
dropdownStyle={dropdownStyle} |
|
||||
allowClear={this.props.form.allowClear} |
|
||||
showSearch={true} |
|
||||
mode={mode} |
|
||||
maxTagTextLength={this.props.form.maxTagTextLength} |
|
||||
disabled={this.props.form.readonly} |
|
||||
optionLabelProp='children' |
|
||||
value={value} |
|
||||
labelInValue={true} |
|
||||
onSelect={this.onSelect} |
|
||||
onDeselect={this.onDeselect} |
|
||||
onFocus={this.onFocus} |
|
||||
onBlur={this.onBlur} |
|
||||
placeholder={this.props.form.placeholder} |
|
||||
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); |
|
||||
@ -1,53 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 { createTheme, ThemeProvider } from '@mui/material/styles'; |
|
||||
import thingsboardTheme from './styles/thingsboardTheme'; |
|
||||
import ThingsboardSchemaForm from './json-form-schema-form'; |
|
||||
import { JsonFormProps } from './json-form.models'; |
|
||||
|
|
||||
const tbTheme = createTheme(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; |
|
||||
@ -1,217 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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'; |
|
||||
import ThingsboardJavaScript from './json-form-javascript'; |
|
||||
import ThingsboardJson from './json-form-json'; |
|
||||
import ThingsboardHtml from './json-form-html'; |
|
||||
import ThingsboardCss from './json-form-css'; |
|
||||
import ThingsboardColor from './json-form-color'; |
|
||||
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'; |
|
||||
import ThingsboardImage from './json-form-image'; |
|
||||
import ThingsboardCheckbox from './json-form-checkbox'; |
|
||||
import ThingsboardHelp from './json-form-help'; |
|
||||
import ThingsboardFieldSet from './json-form-fieldset'; |
|
||||
import ThingsboardIcon from './json-form-icon'; |
|
||||
import { |
|
||||
JsonFormData, |
|
||||
JsonFormProps, |
|
||||
onChangeFn, |
|
||||
OnColorClickFn, onHelpClickFn, |
|
||||
OnIconClickFn, |
|
||||
onToggleFullscreenFn |
|
||||
} from './json-form.models'; |
|
||||
|
|
||||
import _ from 'lodash'; |
|
||||
import tinycolor from 'tinycolor2'; |
|
||||
import { GroupInfo } from '@shared/models/widget.models'; |
|
||||
import ThingsboardMarkdown from '@shared/components/json-form/react/json-form-markdown'; |
|
||||
import { MouseEvent, ReactNode } from 'react'; |
|
||||
|
|
||||
class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> { |
|
||||
|
|
||||
private hasConditions: boolean; |
|
||||
private conditionFunction: Function; |
|
||||
private readonly mapper: {[type: string]: any}; |
|
||||
|
|
||||
constructor(props: JsonFormProps) { |
|
||||
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, |
|
||||
markdown: ThingsboardMarkdown, |
|
||||
color: ThingsboardColor, |
|
||||
'rc-select': ThingsboardRcSelect, |
|
||||
fieldset: ThingsboardFieldSet, |
|
||||
icon: ThingsboardIcon |
|
||||
}; |
|
||||
|
|
||||
this.onChange = this.onChange.bind(this); |
|
||||
this.onColorClick = this.onColorClick.bind(this); |
|
||||
this.onIconClick = this.onIconClick.bind(this); |
|
||||
this.onToggleFullscreen = this.onToggleFullscreen.bind(this); |
|
||||
this.onHelpClick = this.onHelpClick.bind(this); |
|
||||
this.hasConditions = false; |
|
||||
} |
|
||||
|
|
||||
onChange(key: (string | number)[], val: any, forceUpdate?: boolean) { |
|
||||
this.props.onModelChange(key, val, forceUpdate); |
|
||||
if (this.hasConditions) { |
|
||||
this.forceUpdate(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onColorClick(key: (string | number)[], val: tinycolor.ColorFormats.RGBA, |
|
||||
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) { |
|
||||
this.props.onColorClick(key, val, colorSelectedFn); |
|
||||
} |
|
||||
|
|
||||
onIconClick(key: (string | number)[], val: string, |
|
||||
iconSelectedFn: (icon: string) => void) { |
|
||||
this.props.onIconClick(key, val, iconSelectedFn); |
|
||||
} |
|
||||
|
|
||||
onToggleFullscreen(fullscreenFinishFn?: (el: Element) => void) { |
|
||||
this.props.onToggleFullscreen(fullscreenFinishFn); |
|
||||
} |
|
||||
|
|
||||
onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) { |
|
||||
this.props.onHelpClick(event, helpId, helpVisibleFn, helpReadyFn); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
builder(form: JsonFormData, |
|
||||
model: any, |
|
||||
index: number, |
|
||||
onChange: onChangeFn, |
|
||||
onColorClick: OnColorClickFn, |
|
||||
onIconClick: OnIconClickFn, |
|
||||
onToggleFullscreen: onToggleFullscreenFn, |
|
||||
onHelpClick: onHelpClickFn, |
|
||||
mapper: {[type: string]: any}): React.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; |
|
||||
if (!form.conditionFunction) { |
|
||||
form.conditionFunction = new Function('form', 'model', 'index', `return ${form.condition};`); |
|
||||
} |
|
||||
if (form.conditionFunction(form, model, index) === false) { |
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
return <Field model={model} form={form} key={index} onChange={onChange} |
|
||||
onColorClick={onColorClick} |
|
||||
onIconClick={onIconClick} |
|
||||
onToggleFullscreen={onToggleFullscreen} |
|
||||
onHelpClick={onHelpClick} |
|
||||
mapper={mapper} builder={this.builder}/>; |
|
||||
} |
|
||||
|
|
||||
createSchema(theForm: any[]): React.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: ReactNode[] = merged.map(function(form: JsonFormData, index: number) { |
|
||||
return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, |
|
||||
this.onIconClick, this.onToggleFullscreen, this.onHelpClick, 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: React.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: React.JSX.Element; |
|
||||
} |
|
||||
|
|
||||
interface ThingsboardSchemaGroupState { |
|
||||
showGroup: boolean; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardSchemaGroup extends React.Component<ThingsboardSchemaGroupProps, ThingsboardSchemaGroupState> { |
|
||||
constructor(props: ThingsboardSchemaGroupProps) { |
|
||||
super(props); |
|
||||
this.state = { |
|
||||
showGroup: true |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
toogleGroup() { |
|
||||
this.setState({ |
|
||||
showGroup: !this.state.showGroup |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const theCla = 'pull-right fa fa-chevron-down tb-toggle-icon' + (this.state.showGroup ? '' : ' tb-toggled'); |
|
||||
return (<section className='mat-elevation-z1' style={{marginTop: '10px'}}> |
|
||||
<div className='SchemaGroupname tb-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>); |
|
||||
} |
|
||||
} |
|
||||
@ -1,93 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
import MenuItem from '@mui/material/MenuItem'; |
|
||||
import FormControl from '@mui/material/FormControl'; |
|
||||
import InputLabel from '@mui/material/InputLabel'; |
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select'; |
|
||||
import ThingsboardBaseComponent from '@shared/components/json-form/react/json-form-base-component'; |
|
||||
import { isObject } from '@core/utils'; |
|
||||
|
|
||||
interface ThingsboardSelectState extends JsonFormFieldState { |
|
||||
currentValue: any; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardSelect extends React.Component<JsonFormFieldProps, ThingsboardSelectState> { |
|
||||
|
|
||||
static getDerivedStateFromProps(props: JsonFormFieldProps) { |
|
||||
if (props.model && props.form.key) { |
|
||||
return { |
|
||||
currentValue: ThingsboardSelect.getModelKey(props.model, props.form.key) |
|
||||
|| (props.form.titleMap != null ? props.form.titleMap[0].value : '') |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static getModelKey(model: any, key: (string | number)[]) { |
|
||||
if (Array.isArray(key)) { |
|
||||
const res = key.reduce((cur, nxt) => (cur[nxt] || {}), model); |
|
||||
if (res && isObject(res)) { |
|
||||
return undefined; |
|
||||
} else { |
|
||||
return res; |
|
||||
} |
|
||||
} else { |
|
||||
return model[key]; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
super(props); |
|
||||
this.onSelected = this.onSelected.bind(this); |
|
||||
const possibleValue = ThingsboardSelect.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 : '' |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
onSelected(event: SelectChangeEvent<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 variant={'standard'} htmlFor='select-field'>{this.props.form.title}</InputLabel> |
|
||||
<Select |
|
||||
variant={'standard'} |
|
||||
value={this.state.currentValue} |
|
||||
onChange={this.onSelected}> |
|
||||
{menuItems} |
|
||||
</Select> |
|
||||
</FormControl> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardBaseComponent(ThingsboardSelect); |
|
||||
@ -1,92 +0,0 @@ |
|||||
/* |
|
||||
* Copyright © 2016-2024 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 '@mui/material/TextField'; |
|
||||
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; |
|
||||
|
|
||||
interface ThingsboardTextState extends JsonFormFieldState { |
|
||||
focused: boolean; |
|
||||
} |
|
||||
|
|
||||
class ThingsboardText extends React.Component<JsonFormFieldProps, ThingsboardTextState> { |
|
||||
|
|
||||
constructor(props: JsonFormFieldProps) { |
|
||||
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'; |
|
||||
let rows = 1; |
|
||||
let rowsMax = 1; |
|
||||
let minHeight = 48; |
|
||||
if (multiline) { |
|
||||
rows = this.props.form.rows || 2; |
|
||||
rowsMax = this.props.form.rowsMax; |
|
||||
minHeight = 19 * rows + 48; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div> |
|
||||
<TextField |
|
||||
variant={'standard'} |
|
||||
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={(e) => { |
|
||||
this.props.onChangeValidate(e); |
|
||||
}} |
|
||||
defaultValue={this.props.value} |
|
||||
disabled={this.props.form.readonly} |
|
||||
rows={rows} |
|
||||
maxRows={rowsMax} |
|
||||
onFocus={this.onFocus} |
|
||||
onBlur={this.onBlur} |
|
||||
style={this.props.form.style || {width: '100%', minHeight: minHeight + 'px'}}/> |
|
||||
</div> |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export default ThingsboardBaseComponent(ThingsboardText); |
|
||||
@ -1,141 +0,0 @@ |
|||||
///
|
|
||||
/// Copyright © 2016-2024 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 tinycolor from 'tinycolor2'; |
|
||||
import { GroupInfo } from '@shared/models/widget.models'; |
|
||||
import { MouseEvent } 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 type onChangeFn = (key: (string | number)[], val: any, forceUpdate?: boolean) => void; |
|
||||
export type OnColorClickFn = (key: (string | number)[], val: tinycolor.ColorFormats.RGBA, |
|
||||
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) => void; |
|
||||
export type OnIconClickFn = (key: (string | number)[], val: string, |
|
||||
iconSelectedFn: (icon: string) => void) => void; |
|
||||
export type onToggleFullscreenFn = (fullscreenFinishFn?: (el: Element) => void) => void; |
|
||||
export type onHelpClickFn = (event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, |
|
||||
helpReadyFn: (ready: boolean) => void) => void; |
|
||||
|
|
||||
export interface JsonFormProps { |
|
||||
model?: any; |
|
||||
schema?: any; |
|
||||
form?: any; |
|
||||
groupInfoes?: GroupInfo[]; |
|
||||
isFullscreen: boolean; |
|
||||
ignore?: {[key: string]: boolean}; |
|
||||
option: FormOption; |
|
||||
onModelChange?: onChangeFn; |
|
||||
onColorClick?: OnColorClickFn; |
|
||||
onIconClick?: OnIconClickFn; |
|
||||
onToggleFullscreen?: onToggleFullscreenFn; |
|
||||
onHelpClick?: onHelpClickFn; |
|
||||
mapper?: {[type: string]: any}; |
|
||||
} |
|
||||
|
|
||||
export interface KeyLabelItem { |
|
||||
key: string; |
|
||||
label: string; |
|
||||
value?: string; |
|
||||
} |
|
||||
|
|
||||
export interface JsonSchemaData { |
|
||||
type: string; |
|
||||
default: any; |
|
||||
items?: JsonSchemaData; |
|
||||
properties?: any; |
|
||||
} |
|
||||
|
|
||||
export interface JsonFormData { |
|
||||
type: string; |
|
||||
key: (string | number)[]; |
|
||||
title: string; |
|
||||
readonly: boolean; |
|
||||
required: boolean; |
|
||||
default?: any; |
|
||||
condition?: string; |
|
||||
conditionFunction?: Function; |
|
||||
style?: any; |
|
||||
rows?: number; |
|
||||
rowsMax?: number; |
|
||||
placeholder?: string; |
|
||||
schema: JsonSchemaData; |
|
||||
titleMap: { |
|
||||
value: any; |
|
||||
name: string; |
|
||||
}[]; |
|
||||
items?: Array<KeyLabelItem> | Array<JsonFormData>; |
|
||||
tabs?: Array<JsonFormData>; |
|
||||
tags?: any; |
|
||||
helpId?: string; |
|
||||
startEmpty?: boolean; |
|
||||
[key: string]: any; |
|
||||
} |
|
||||
|
|
||||
export type ComponentBuilderFn = (form: JsonFormData, |
|
||||
model: any, |
|
||||
index: number, |
|
||||
onChange: onChangeFn, |
|
||||
onColorClick: OnColorClickFn, |
|
||||
onIconClick: OnIconClickFn, |
|
||||
onToggleFullscreen: onToggleFullscreenFn, |
|
||||
onHelpClick: onHelpClickFn, |
|
||||
mapper: {[type: string]: any}) => JSX.Element; |
|
||||
|
|
||||
export interface JsonFormFieldProps { |
|
||||
value: any; |
|
||||
model: any; |
|
||||
form: JsonFormData; |
|
||||
builder: ComponentBuilderFn; |
|
||||
mapper?: {[type: string]: any}; |
|
||||
onChange?: onChangeFn; |
|
||||
onColorClick?: OnColorClickFn; |
|
||||
onIconClick?: OnIconClickFn; |
|
||||
onChangeValidate?: (e: any, forceUpdate?: boolean) => void; |
|
||||
onToggleFullscreen?: onToggleFullscreenFn; |
|
||||
onHelpClick?: onHelpClickFn; |
|
||||
valid?: boolean; |
|
||||
error?: string; |
|
||||
options?: { |
|
||||
setSchemaDefaults?: boolean; |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
export interface JsonFormFieldState { |
|
||||
value?: any; |
|
||||
valid?: boolean; |
|
||||
error?: string; |
|
||||
} |
|
||||
@ -1,361 +0,0 @@ |
|||||
/** |
|
||||
* Copyright © 2016-2024 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. |
|
||||
*/ |
|
||||
$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; |
|
||||
|
|
||||
$previewSize: 100px !default; |
|
||||
|
|
||||
.tb-json-form { |
|
||||
|
|
||||
&.tb-fullscreen { |
|
||||
background: #fff; |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
right: 0; |
|
||||
bottom: 0; |
|
||||
left: 0; |
|
||||
> div.fullscreen-form-field { |
|
||||
position: relative; |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.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; |
|
||||
box-sizing: border-box; |
|
||||
padding: 10px 0; |
|
||||
margin-top: 32px; |
|
||||
} |
|
||||
|
|
||||
.tb-field { |
|
||||
padding-bottom: 18px; |
|
||||
|
|
||||
.MuiInputBase-multiline { |
|
||||
flex: 1; |
|
||||
flex-direction: column; |
|
||||
.MuiInputBase-inputMultiline { |
|
||||
flex: 1; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
&.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); |
|
||||
padding-bottom: 15px; |
|
||||
} |
|
||||
|
|
||||
.SchemaGroupname { |
|
||||
padding: 10px 20px; |
|
||||
background-color: #f1f1f1; |
|
||||
} |
|
||||
|
|
||||
.invisible { |
|
||||
display: none; |
|
||||
} |
|
||||
|
|
||||
.tb-button-toggle .tb-toggle-icon { |
|
||||
display: inline-block; |
|
||||
width: 15px; |
|
||||
margin: auto 0 auto auto; |
|
||||
background-size: 100% auto; |
|
||||
|
|
||||
transition: transform .3s, ease-in-out; |
|
||||
} |
|
||||
|
|
||||
.tb-button-toggle .tb-toggle-icon.tb-toggled { |
|
||||
transform: rotateZ(180deg); |
|
||||
} |
|
||||
|
|
||||
.fullscreen-form-field { |
|
||||
.json-form-ace-editor { |
|
||||
height: calc(100% - 60px); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.json-form-ace-editor { |
|
||||
position: relative; |
|
||||
height: 100%; |
|
||||
border: 1px solid #c0c0c0; |
|
||||
|
|
||||
.title-panel { |
|
||||
position: absolute; |
|
||||
top: 10px; |
|
||||
right: 20px; |
|
||||
z-index: 5; |
|
||||
font-size: .8rem; |
|
||||
font-weight: 500; |
|
||||
|
|
||||
label { |
|
||||
padding: 4px; |
|
||||
color: #00acc1; |
|
||||
background: rgba(220, 220, 220, .35); |
|
||||
border-radius: 5px; |
|
||||
} |
|
||||
|
|
||||
button.tidy-button { |
|
||||
background: rgba(220, 220, 220, .35) !important; |
|
||||
|
|
||||
span { |
|
||||
padding: 0 !important; |
|
||||
font-size: 12px !important; |
|
||||
} |
|
||||
} |
|
||||
button.help-button { |
|
||||
background: rgba(220, 220, 220, .35); |
|
||||
padding: 4px; |
|
||||
} |
|
||||
div.help-button-loading { |
|
||||
pointer-events: none; |
|
||||
background: #f3f3f3; |
|
||||
border-radius: 50%; |
|
||||
display: flex; |
|
||||
place-content: center; |
|
||||
align-items: center; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.tb-image-select-container { |
|
||||
position: relative; |
|
||||
width: 100%; |
|
||||
height: $previewSize; |
|
||||
} |
|
||||
|
|
||||
.tb-image-preview { |
|
||||
width: auto; |
|
||||
max-width: $previewSize; |
|
||||
height: auto; |
|
||||
max-height: $previewSize; |
|
||||
} |
|
||||
|
|
||||
.tb-image-preview-container { |
|
||||
position: relative; |
|
||||
float: left; |
|
||||
width: $previewSize; |
|
||||
height: $previewSize; |
|
||||
margin-right: 12px; |
|
||||
vertical-align: top; |
|
||||
border: solid 1px; |
|
||||
|
|
||||
div { |
|
||||
width: 100%; |
|
||||
font-size: 18px; |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
div, .tb-image-preview { |
|
||||
position: absolute; |
|
||||
top: 50%; |
|
||||
left: 50%; |
|
||||
transform: translate(-50%, -50%); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.tb-dropzone { |
|
||||
outline: none; |
|
||||
position: relative; |
|
||||
height: $previewSize; |
|
||||
padding: 0 8px; |
|
||||
overflow: hidden; |
|
||||
vertical-align: top; |
|
||||
border: dashed 2px; |
|
||||
|
|
||||
div { |
|
||||
position: absolute; |
|
||||
top: 50%; |
|
||||
left: 50%; |
|
||||
width: 100%; |
|
||||
font-size: 24px; |
|
||||
text-align: center; |
|
||||
transform: translate(-50%, -50%); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.tb-image-clear-container { |
|
||||
position: relative; |
|
||||
float: right; |
|
||||
width: 48px; |
|
||||
height: $previewSize; |
|
||||
} |
|
||||
|
|
||||
.tb-image-clear-btn { |
|
||||
position: absolute !important; |
|
||||
top: 50%; |
|
||||
transform: translate(0%, -50%) !important; |
|
||||
} |
|
||||
|
|
||||
.MuiButton-root { |
|
||||
text-transform: none; |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
|
|
||||
.rc-select { |
|
||||
box-sizing: border-box; |
|
||||
display: inline-block; |
|
||||
position: relative; |
|
||||
vertical-align: middle; |
|
||||
color: #666; |
|
||||
line-height: 28px; |
|
||||
font-size: inherit !important; |
|
||||
.rc-select-selector { |
|
||||
outline: none; |
|
||||
user-select: none; |
|
||||
box-sizing: border-box; |
|
||||
display: block; |
|
||||
background-color: #fff; |
|
||||
border-radius: 6px; |
|
||||
} |
|
||||
&.rc-select-single { |
|
||||
&:not(.rc-select-customize-input) { |
|
||||
.rc-select-selector { |
|
||||
height: 28px; |
|
||||
line-height: 28px; |
|
||||
position: relative; |
|
||||
border: 1px solid #d9d9d9; |
|
||||
&:hover { |
|
||||
border-color: #23c0fa; |
|
||||
box-shadow: 0 0 2px rgba(45, 183, 245, 0.8); |
|
||||
} |
|
||||
.rc-select-selection-search { |
|
||||
.rc-select-selection-search-input { |
|
||||
cursor: pointer; |
|
||||
background: transparent; |
|
||||
margin-left: 10px; |
|
||||
} |
|
||||
} |
|
||||
.rc-select-selection-item, .rc-select-selection-placeholder { |
|
||||
top: 0; |
|
||||
left: 10px; |
|
||||
} |
|
||||
} |
|
||||
&.rc-select-focused { |
|
||||
.rc-select-selector { |
|
||||
border-color: #23c0fa !important; |
|
||||
box-shadow: 0 0 2px rgba(45, 183, 245, 0.8) !important; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.rc-select-dropdown { |
|
||||
&.tb-rc-select-dropdown { |
|
||||
z-index: 100001; |
|
||||
background-color: white; |
|
||||
border: 1px solid #d9d9d9; |
|
||||
box-shadow: 0 0 4px #d9d9d9; |
|
||||
border-radius: 4px; |
|
||||
box-sizing: border-box; |
|
||||
outline: none; |
|
||||
|
|
||||
.rc-select-item { |
|
||||
&.rc-select-item-option { |
|
||||
margin: 0; |
|
||||
position: relative; |
|
||||
display: block; |
|
||||
padding: 7px 10px; |
|
||||
font-weight: normal; |
|
||||
color: #666; |
|
||||
white-space: nowrap; |
|
||||
&.rc-select-item-option-selected { |
|
||||
color: #666; |
|
||||
background-color: #ddd; |
|
||||
} |
|
||||
&.rc-select-item-option-active { |
|
||||
background-color: #5897fb; |
|
||||
color: white; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,46 +0,0 @@ |
|||||
///
|
|
||||
/// Copyright © 2016-2024 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, deepOrange } from '@mui/material/colors'; |
|
||||
import { ThemeOptions } from '@mui/material/styles'; |
|
||||
import { PaletteOptions } from '@mui/material/styles/createPalette'; |
|
||||
import { mergeDeep } from '@core/utils'; |
|
||||
|
|
||||
const PRIMARY_COLOR = '#305680'; |
|
||||
const SECONDARY_COLOR = '#527dad'; |
|
||||
const HUE3_COLOR = '#a7c1de'; |
|
||||
|
|
||||
const tbIndigo = mergeDeep<any>({}, indigo, { |
|
||||
500: PRIMARY_COLOR, |
|
||||
600: SECONDARY_COLOR, |
|
||||
700: PRIMARY_COLOR, |
|
||||
A100: HUE3_COLOR |
|
||||
}); |
|
||||
|
|
||||
const thingsboardPalette: PaletteOptions = { |
|
||||
primary: tbIndigo, |
|
||||
secondary: deepOrange, |
|
||||
background: { |
|
||||
default: '#eee' |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
export default { |
|
||||
typography: { |
|
||||
fontFamily: 'Roboto, \'Helvetica Neue\', sans-serif' |
|
||||
}, |
|
||||
palette: thingsboardPalette, |
|
||||
} as ThemeOptions; |
|
||||
@ -0,0 +1,60 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 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 '../../../../scss/constants'; |
||||
|
|
||||
|
:host-context(.tb-form-row) { |
||||
|
:host { |
||||
|
display: flex; |
||||
|
} |
||||
|
.mat-mdc-form-field { |
||||
|
&.tb-datetime-field { |
||||
|
width: 185px; |
||||
|
&.with-clear { |
||||
|
width: 225px; |
||||
|
} |
||||
|
} |
||||
|
&.tb-date-field { |
||||
|
width: 140px; |
||||
|
&.with-clear { |
||||
|
width: 180px; |
||||
|
} |
||||
|
} |
||||
|
&.tb-time-field { |
||||
|
width: 100px; |
||||
|
&.with-clear { |
||||
|
width: 140px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host-context(.tb-form-row.column-xs) { |
||||
|
@media #{$mat-xs} { |
||||
|
.mat-mdc-form-field { |
||||
|
&.tb-datetime-field, &.tb-date-field, &.tb-time-field { |
||||
|
width: auto; |
||||
|
flex: 1; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
:host { |
||||
|
.mat-mdc-form-field-icon-suffix, |
||||
|
.mat-datetimepicker-toggle { |
||||
|
color: rgba(0, 0, 0, 0.38); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue