44 changed files with 2772 additions and 1297 deletions
@ -0,0 +1,212 @@ |
|||||
|
///
|
||||
|
/// 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 { ValueType } from '@shared/models/constants'; |
||||
|
import { |
||||
|
FormProperty, |
||||
|
formPropertyFieldClasses, |
||||
|
formPropertyRowClasses, |
||||
|
FormPropertyType, |
||||
|
formPropertyTypes, |
||||
|
formPropertyTypeTranslations, |
||||
|
FormSelectItem |
||||
|
} from '@shared/models/dynamic-form.models'; |
||||
|
import { |
||||
|
defaultPropertyValue |
||||
|
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
|
||||
|
@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; |
||||
|
|
||||
|
formPropertyTypes = formPropertyTypes; |
||||
|
formPropertyTypeTranslations = formPropertyTypeTranslations; |
||||
|
|
||||
|
formPropertyRowClasses = formPropertyRowClasses; |
||||
|
|
||||
|
formPropertyFieldClasses = formPropertyFieldClasses; |
||||
|
|
||||
|
@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; |
||||
|
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]], |
||||
|
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 || '').split(' '), []], |
||||
|
fieldClass: [(this.property.fieldClass || '').split(' '), []], |
||||
|
min: [this.property.min, []], |
||||
|
max: [this.property.max, []], |
||||
|
step: [this.property.step, [Validators.min(0)]], |
||||
|
properties: [this.property.properties, []], |
||||
|
multiple: [this.property.multiple, []], |
||||
|
items: [this.property.items, []] |
||||
|
} |
||||
|
); |
||||
|
if (this.disabled) { |
||||
|
this.propertyFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('type').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() { |
||||
|
const type: FormPropertyType = this.propertyFormGroup.get('type').value; |
||||
|
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 (type === FormPropertyType.select) { |
||||
|
this.propertyFormGroup.get('multiple').enable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('items').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.propertyFormGroup.get('multiple').disable({emitEvent: false}); |
||||
|
this.propertyFormGroup.get('items').disable({emitEvent: false}); |
||||
|
} |
||||
|
if (this.propertyType !== type) { |
||||
|
const defaultValue = defaultPropertyValue(type); |
||||
|
this.propertyFormGroup.get('default').patchValue(defaultValue, {emitEvent: false}); |
||||
|
this.propertyType = type; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private onSelectItemsChange() { |
||||
|
const type: FormPropertyType = this.propertyFormGroup.get('type').value; |
||||
|
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 && type === FormPropertyType.select) { |
||||
|
if (multiple) { |
||||
|
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: FormPropertyType = this.propertyFormGroup.get('type').value; |
||||
|
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}); |
||||
|
} |
||||
|
} else { |
||||
|
if (defaultValue && Array.isArray(defaultValue)) { |
||||
|
const newVal = defaultValue.length ? defaultValue[0] : null; |
||||
|
setTimeout(() => { |
||||
|
this.propertyFormGroup.get('default').patchValue(newVal, {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,23 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
.tb-dynamic-form-select-item-row { |
||||
|
.tb-value-field { |
||||
|
flex: 1 1 70%; |
||||
|
} |
||||
|
.tb-label-field { |
||||
|
flex: 1 1 30%; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,172 @@ |
|||||
|
///
|
||||
|
/// 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 { TimeSeriesChartStateSourceType } from '@home/components/widget/lib/chart/time-series-chart.models'; |
||||
|
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,61 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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-select-items no-border no-padding"> |
||||
|
<div class="tb-form-table"> |
||||
|
<div class="tb-form-table-header" [class.no-padding-right]="!disabled"> |
||||
|
<div class="tb-form-table-header-cell tb-value-header" translate>dynamic-form.property.value</div> |
||||
|
<div class="tb-form-table-header-cell tb-label-header" translate>dynamic-form.property.label</div> |
||||
|
<div class="tb-form-table-header-cell tb-actions-header" [class.disabled]="disabled"></div> |
||||
|
</div> |
||||
|
<div *ngIf="selectItemsFormArray().controls.length; else noSelectItems" class="tb-form-table-body tb-drop-list" |
||||
|
cdkDropList cdkDropListOrientation="vertical" |
||||
|
[cdkDropListDisabled]="!dragEnabled" |
||||
|
(cdkDropListDropped)="selectItemDrop($event)"> |
||||
|
<div cdkDrag [cdkDragDisabled]="!dragEnabled" |
||||
|
[class.tb-draggable-form-table-row]="!disabled" |
||||
|
*ngFor="let selectItemControl of selectItemsFormArray().controls; trackBy: trackBySelectItem; let $index = index;"> |
||||
|
<tb-dynamic-form-select-item-row class="flex-1" |
||||
|
[index]="$index" |
||||
|
[formControl]="selectItemControl" |
||||
|
(selectItemRemoved)="removeSelectItem($index)"> |
||||
|
</tb-dynamic-form-select-item-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)="addSelectItem()"> |
||||
|
{{ 'dynamic-form.property.add-option' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<ng-template #noSelectItems> |
||||
|
<span class="tb-prompt flex items-center justify-center">{{ 'dynamic-form.property.no-options' | translate }}</span> |
||||
|
</ng-template> |
||||
@ -0,0 +1,43 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
.tb-dynamic-form-select-items { |
||||
|
flex: 1; |
||||
|
.tb-form-table-header-cell { |
||||
|
&.tb-value-header { |
||||
|
flex: 1 1 70%; |
||||
|
} |
||||
|
&.tb-label-header { |
||||
|
flex: 1 1 30%; |
||||
|
} |
||||
|
&.tb-actions-header { |
||||
|
width: 80px; |
||||
|
min-width: 80px; |
||||
|
&.disabled { |
||||
|
width: 0; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.tb-form-table { |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
.tb-form-table-body { |
||||
|
overflow: auto; |
||||
|
tb-dynamic-form-select-item-row { |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
itemsArray.insert(event.currentIndex, item); |
||||
|
} |
||||
|
|
||||
|
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,117 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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" |
||||
|
[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.type === FormPropertyContainerType.row && propertyContainer.visible"> |
||||
|
<ng-container *ngTemplateOutlet="propertyRowTpl;context:{propertyRow: propertyContainer}"> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
<tb-dynamic-form *ngIf="propertyContainer.type === FormPropertyContainerType.fieldset && propertyContainer.visible" |
||||
|
formControlName="{{ propertyContainer.property.id }}" |
||||
|
title="{{ propertyContainer.label | customTranslate }}" |
||||
|
stroked |
||||
|
[properties]="propertyContainer.properties"> |
||||
|
</tb-dynamic-form> |
||||
|
</ng-container> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #propertyRowTpl let-propertyRow="propertyRow"> |
||||
|
<div [formGroup]="propertiesFormGroup" class="tb-form-row space-between flex-wrap 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 row flex-end align-center"> |
||||
|
<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> |
||||
|
<mat-form-field *ngIf="property.type === FormPropertyType.text" [class]="property.fieldClass" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput 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.select" [class]="property.fieldClass || 'medium-width'" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="{{ property.id }}" [multiple]="property.multiple"> |
||||
|
<mat-option *ngFor="let item of property.items" [value]="item.value"> |
||||
|
{{ item.label | customTranslate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<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> |
||||
|
<mat-form-field *ngIf="property.type === FormPropertyType.number" [class]="property.fieldClass" appearance="outline" class="number" subscriptSizing="dynamic"> |
||||
|
<input matInput formControlName="{{ property.id }}" [required]="property.required" |
||||
|
[min]="property.min" [max]="property.max" [step]="property.step" |
||||
|
type="number" placeholder="{{ 'widget-config.set' | translate }}"> |
||||
|
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div> |
||||
|
</mat-form-field> |
||||
|
<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" |
||||
|
asBoxInput |
||||
|
formControlName="{{ property.id }}"> |
||||
|
</tb-material-icon-select> |
||||
|
<mat-divider *ngIf="property.divider" vertical></mat-divider> |
||||
|
</ng-container> |
||||
|
</ng-container> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-template> |
||||
@ -0,0 +1,22 @@ |
|||||
|
/** |
||||
|
* 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. |
||||
|
*/ |
||||
|
:host ::ng-deep { |
||||
|
.tb-properties-content { |
||||
|
gap: 16px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,265 @@ |
|||||
|
///
|
||||
|
/// 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, |
||||
|
PropertyConditionFunction, |
||||
|
toPropertyGroups |
||||
|
} from '@shared/models/dynamic-form.models'; |
||||
|
import { coerceBoolean } from '@shared/decorators/coercion'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
|
||||
|
@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 { |
||||
|
|
||||
|
FormPropertyContainerType = FormPropertyContainerType; |
||||
|
|
||||
|
FormPropertyType = FormPropertyType; |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
@Input() |
||||
|
properties: FormProperty[]; |
||||
|
|
||||
|
@Input() |
||||
|
title: string; |
||||
|
|
||||
|
@Input() |
||||
|
@coerceBoolean() |
||||
|
stroked = false; |
||||
|
|
||||
|
private modelValue: {[id: string]: any}; |
||||
|
|
||||
|
private propagateChange = null; |
||||
|
|
||||
|
private validatorTriggers: string[]; |
||||
|
|
||||
|
public propertiesFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
propertyGroups: FormPropertyGroup[]; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
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); |
||||
|
for (const property of this.properties) { |
||||
|
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)); |
||||
|
} |
||||
|
} |
||||
|
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 (container.type === FormPropertyContainerType.fieldset) { |
||||
|
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) { |
||||
|
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,136 +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, 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'; |
|
||||
|
|
||||
@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) { |
|
||||
} |
|
||||
|
|
||||
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.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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
File diff suppressed because it is too large
@ -0,0 +1,385 @@ |
|||||
|
///
|
||||
|
/// 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 { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; |
||||
|
import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models'; |
||||
|
import { isString } from '@core/utils'; |
||||
|
import { JsonSchema, JsonSettingsSchema } from '@shared/models/widget.models'; |
||||
|
import JsonFormUtils from '@shared/components/json-form/react/json-form-utils'; |
||||
|
import { JsonFormData, KeyLabelItem } from '@shared/components/json-form/react/json-form.models'; |
||||
|
|
||||
|
export enum FormPropertyType { |
||||
|
text = 'text', |
||||
|
number = 'number', |
||||
|
switch = 'switch', |
||||
|
select = 'select', |
||||
|
color = 'color', |
||||
|
color_settings = 'color_settings', |
||||
|
font = 'font', |
||||
|
units = 'units', |
||||
|
icon = 'icon', |
||||
|
fieldset = 'fieldset' |
||||
|
} |
||||
|
|
||||
|
export const formPropertyTypes = Object.keys(FormPropertyType) as FormPropertyType[]; |
||||
|
|
||||
|
export const formPropertyTypeTranslations = new Map<FormPropertyType, string>( |
||||
|
[ |
||||
|
[FormPropertyType.text, 'dynamic-form.property.type-text'], |
||||
|
[FormPropertyType.number, 'dynamic-form.property.type-number'], |
||||
|
[FormPropertyType.switch, 'dynamic-form.property.type-switch'], |
||||
|
[FormPropertyType.select, 'dynamic-form.property.type-select'], |
||||
|
[FormPropertyType.color, 'dynamic-form.property.type-color'], |
||||
|
[FormPropertyType.color_settings, 'dynamic-form.property.type-color-settings'], |
||||
|
[FormPropertyType.font, 'dynamic-form.property.type-font'], |
||||
|
[FormPropertyType.units, 'dynamic-form.property.type-units'], |
||||
|
[FormPropertyType.icon, 'dynamic-form.property.type-icon'], |
||||
|
[FormPropertyType.fieldset, 'dynamic-form.property.type-fieldset'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export const formPropertyRowClasses = |
||||
|
['column', 'column-xs', 'column-lt-md', 'align-start', 'no-border', 'no-gap', 'no-padding', 'same-padding']; |
||||
|
|
||||
|
export const formPropertyFieldClasses = |
||||
|
['medium-width', 'flex', 'flex-xs', 'flex-lt-md']; |
||||
|
|
||||
|
export type PropertyConditionFunction = (property: FormProperty, model: any) => boolean; |
||||
|
|
||||
|
export interface FormPropertyBase { |
||||
|
id: string; |
||||
|
name: string; |
||||
|
group?: string; |
||||
|
type: FormPropertyType; |
||||
|
default: any; |
||||
|
required?: boolean; |
||||
|
subLabel?: string; |
||||
|
divider?: boolean; |
||||
|
fieldSuffix?: string; |
||||
|
disableOnProperty?: string; |
||||
|
condition?: string; |
||||
|
conditionFunction?: PropertyConditionFunction; |
||||
|
disabled?: boolean; |
||||
|
visible?: boolean; |
||||
|
rowClass?: string; |
||||
|
fieldClass?: string; |
||||
|
} |
||||
|
|
||||
|
export interface FormNumberProperty extends FormPropertyBase { |
||||
|
min?: number; |
||||
|
max?: number; |
||||
|
step?: number; |
||||
|
} |
||||
|
|
||||
|
export interface FormFieldSetProperty extends FormPropertyBase { |
||||
|
properties?: FormProperty[]; |
||||
|
} |
||||
|
|
||||
|
export interface FormSelectItem { |
||||
|
value: any; |
||||
|
label: string; |
||||
|
} |
||||
|
|
||||
|
export interface FormSelectProperty extends FormPropertyBase { |
||||
|
multiple?: boolean; |
||||
|
items?: FormSelectItem[]; |
||||
|
} |
||||
|
|
||||
|
export type FormProperty = FormPropertyBase & FormNumberProperty & FormSelectProperty & FormFieldSetProperty; |
||||
|
|
||||
|
export enum FormPropertyContainerType { |
||||
|
row = 'row', |
||||
|
fieldset = 'fieldset' |
||||
|
} |
||||
|
|
||||
|
export interface FormPropertyContainerBase { |
||||
|
type: FormPropertyContainerType; |
||||
|
label: string; |
||||
|
properties: FormProperty[]; |
||||
|
visible: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface FormPropertyRow extends FormPropertyContainerBase { |
||||
|
switch?: FormProperty; |
||||
|
rowClass?: string; |
||||
|
} |
||||
|
|
||||
|
export interface FormPropertyFieldset extends FormPropertyContainerBase { |
||||
|
property?: FormProperty; |
||||
|
} |
||||
|
|
||||
|
export type FormPropertyContainer = FormPropertyRow & FormPropertyFieldset; |
||||
|
|
||||
|
export interface FormPropertyGroup { |
||||
|
title?: string; |
||||
|
containers: FormPropertyContainer[]; |
||||
|
visible: boolean; |
||||
|
} |
||||
|
|
||||
|
export const toPropertyGroups = (properties: FormProperty[]): FormPropertyGroup[] => { |
||||
|
const groups: {title: string, properties: FormProperty[]}[] = []; |
||||
|
for (let property of properties) { |
||||
|
if (!property.group) { |
||||
|
groups.push({ |
||||
|
title: null, |
||||
|
properties: [property] |
||||
|
}); |
||||
|
} else { |
||||
|
let propertyGroup = groups.find(g => g.title === property.group); |
||||
|
if (!propertyGroup) { |
||||
|
propertyGroup = { |
||||
|
title: property.group, |
||||
|
properties: [] |
||||
|
}; |
||||
|
groups.push(propertyGroup); |
||||
|
} |
||||
|
propertyGroup.properties.push(property); |
||||
|
} |
||||
|
} |
||||
|
return groups.map(g => ({ |
||||
|
title: g.title, |
||||
|
containers: toPropertyContainers(g.properties), |
||||
|
visible: true |
||||
|
})); |
||||
|
}; |
||||
|
|
||||
|
const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer[] => { |
||||
|
const result: FormPropertyContainer[] = []; |
||||
|
for (let property of properties) { |
||||
|
if (property.type === FormPropertyType.fieldset) { |
||||
|
const propertyFieldset: FormPropertyFieldset = { |
||||
|
property, |
||||
|
label: property.name, |
||||
|
type: FormPropertyContainerType.fieldset, |
||||
|
properties: property.properties, |
||||
|
visible: true |
||||
|
}; |
||||
|
result.push(propertyFieldset); |
||||
|
} else { |
||||
|
let propertyRow = |
||||
|
result.find(r => r.type === FormPropertyContainerType.row && r.label === property.name); |
||||
|
if (!propertyRow) { |
||||
|
propertyRow = { |
||||
|
label: property.name, |
||||
|
type: FormPropertyContainerType.row, |
||||
|
properties: [], |
||||
|
rowClass: property.rowClass, |
||||
|
visible: true |
||||
|
}; |
||||
|
result.push(propertyRow); |
||||
|
} |
||||
|
if (property.type === FormPropertyType.switch) { |
||||
|
propertyRow.switch = property; |
||||
|
} else { |
||||
|
propertyRow.properties.push(property); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
export const defaultFormProperties = (properties: FormProperty[]): {[id: string]: any} => { |
||||
|
const formProperties: {[id: string]: any} = {}; |
||||
|
for (const property of properties) { |
||||
|
formProperties[property.id] = defaultFormPropertyValue(property); |
||||
|
} |
||||
|
return formProperties; |
||||
|
}; |
||||
|
|
||||
|
export const defaultFormPropertyValue = (property: FormProperty): any => { |
||||
|
if (property.type === FormPropertyType.fieldset) { |
||||
|
const propertyValue: {[id: string]: any} = {}; |
||||
|
for (const childProperty of property.properties) { |
||||
|
propertyValue[childProperty.id] = defaultFormPropertyValue(childProperty); |
||||
|
} |
||||
|
return propertyValue; |
||||
|
} else { |
||||
|
return property.default; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const formPropertyCompletions = (properties: FormProperty[], customTranslate: CustomTranslatePipe): TbEditorCompletions => { |
||||
|
const propertiesCompletions: TbEditorCompletions = {}; |
||||
|
for (const property of properties) { |
||||
|
propertiesCompletions[property.id] = formPropertyCompletion(property, customTranslate); |
||||
|
} |
||||
|
return propertiesCompletions; |
||||
|
} |
||||
|
|
||||
|
export const formPropertyCompletion = (property: FormProperty, customTranslate: CustomTranslatePipe): TbEditorCompletion => { |
||||
|
let description = customTranslate.transform(property.name, property.name); |
||||
|
if (property.subLabel) { |
||||
|
description += ` <small>${customTranslate.transform(property.subLabel, property.subLabel)}</small>`; |
||||
|
} |
||||
|
if (property.type === FormPropertyType.select) { |
||||
|
if (property.multiple) { |
||||
|
description += '<br/><br/><code class="title"><b>Possible values of array element:</b></code>'; |
||||
|
} else { |
||||
|
description += '<br/><br/><code class="title"><b>Possible values:</b></code>'; |
||||
|
} |
||||
|
description += `<ul>${property.items.map(item => `<li>${item.value} <code style="font-size: 11px;">${typeof item.value}</code></li>`).join('\n')}</ul>`; |
||||
|
} |
||||
|
const completion: TbEditorCompletion = { |
||||
|
meta: 'property', |
||||
|
description, |
||||
|
type: formPropertyCompletionType(property) |
||||
|
}; |
||||
|
if (property.type === FormPropertyType.fieldset) { |
||||
|
completion.children = {}; |
||||
|
for (const childProperty of property.properties) { |
||||
|
completion.children[childProperty.id] = formPropertyCompletion(childProperty, customTranslate); |
||||
|
} |
||||
|
} |
||||
|
return completion; |
||||
|
}; |
||||
|
|
||||
|
const formPropertyCompletionType = (property: FormProperty): string => { |
||||
|
switch (property.type) { |
||||
|
case FormPropertyType.text: |
||||
|
return 'string'; |
||||
|
case FormPropertyType.number: |
||||
|
return 'number'; |
||||
|
case FormPropertyType.switch: |
||||
|
return 'boolean'; |
||||
|
case FormPropertyType.select: |
||||
|
const items = property.items || []; |
||||
|
const types: string[] = []; |
||||
|
items.forEach(item => { |
||||
|
const type = typeof item.value; |
||||
|
if (!types.includes(type)) { |
||||
|
types.push(type); |
||||
|
} |
||||
|
}); |
||||
|
const typesString = types.length ? types.join(' | ') : 'string'; |
||||
|
if (property.multiple) { |
||||
|
return `Array<${typesString}>`; |
||||
|
} else { |
||||
|
return typesString; |
||||
|
} |
||||
|
case FormPropertyType.color: |
||||
|
return 'color string'; |
||||
|
case FormPropertyType.color_settings: |
||||
|
return 'ColorProcessor'; |
||||
|
case FormPropertyType.font: |
||||
|
return 'Font'; |
||||
|
case FormPropertyType.units: |
||||
|
return 'units string'; |
||||
|
case FormPropertyType.icon: |
||||
|
return 'icon string'; |
||||
|
case FormPropertyType.fieldset: |
||||
|
return 'object'; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
export const jsonFormSchemaToFormProperties = (rawSchema: string | any) : FormProperty[] => { |
||||
|
const properties: FormProperty[] = []; |
||||
|
let settingsSchema: JsonSettingsSchema; |
||||
|
if (!rawSchema || rawSchema === '') { |
||||
|
settingsSchema = {}; |
||||
|
} else { |
||||
|
settingsSchema = isString(rawSchema) ? JSON.parse(rawSchema) : rawSchema; |
||||
|
} |
||||
|
if (settingsSchema.schema) { |
||||
|
const schema = settingsSchema.schema; |
||||
|
const form = settingsSchema.form || ['*']; |
||||
|
const groupInfoes = settingsSchema.groupInfoes || []; |
||||
|
if (form.length > 0) { |
||||
|
if (groupInfoes.length) { |
||||
|
for (const info of groupInfoes) { |
||||
|
const theForm: any[] = form[info.formIndex]; |
||||
|
properties.push(...schemaFormToProperties(schema, theForm, info.GroupTitle)); |
||||
|
} |
||||
|
} else { |
||||
|
properties.push(...schemaFormToProperties(schema, form)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return properties; |
||||
|
} |
||||
|
|
||||
|
const schemaFormToProperties = (schema: JsonSchema, theForm: any[], groupTitle?: string): FormProperty[] => { |
||||
|
const merged = JsonFormUtils.merge(schema, theForm, {}, { |
||||
|
formDefaults: { |
||||
|
startEmpty: true |
||||
|
} |
||||
|
}); |
||||
|
return merged.map((form: JsonFormData) => jsonFormDataToProperty(form, 0, groupTitle)).filter(p => p != null); |
||||
|
} |
||||
|
|
||||
|
const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: string): FormProperty => { |
||||
|
if (form.key && form.key.length > level) { |
||||
|
const property: FormProperty = { |
||||
|
id: form.key[level] + '', |
||||
|
name: form.title, |
||||
|
group: groupTitle, |
||||
|
type: null, |
||||
|
default: form.schema.default, |
||||
|
required: form.required |
||||
|
}; |
||||
|
if (form.condition?.length) { |
||||
|
property.condition = `return ${form.condition};`; |
||||
|
} |
||||
|
switch (form.type) { |
||||
|
case 'number': |
||||
|
property.type = FormPropertyType.number; |
||||
|
break; |
||||
|
case 'text': |
||||
|
property.type = FormPropertyType.text; |
||||
|
property.fieldClass = 'flex'; |
||||
|
break; |
||||
|
case 'checkbox': |
||||
|
property.type = FormPropertyType.switch; |
||||
|
break; |
||||
|
case 'rc-select': |
||||
|
property.type = FormPropertyType.select; |
||||
|
if (form.items?.length) { |
||||
|
property.items = (form.items as KeyLabelItem[]).map(item => ({value: item.value, label: item.label})); |
||||
|
} else { |
||||
|
property.items = []; |
||||
|
} |
||||
|
property.multiple = form.multiple; |
||||
|
property.fieldClass = 'flex'; |
||||
|
break; |
||||
|
case 'select': |
||||
|
property.type = FormPropertyType.select; |
||||
|
if (form.titleMap?.length) { |
||||
|
property.items = form.titleMap.map(item => ({value: item.value, label: item.name})); |
||||
|
} else { |
||||
|
property.items = []; |
||||
|
} |
||||
|
property.multiple = false; |
||||
|
property.fieldClass = 'flex'; |
||||
|
break; |
||||
|
case 'color': |
||||
|
property.type = FormPropertyType.color; |
||||
|
break; |
||||
|
case 'icon': |
||||
|
property.type = FormPropertyType.icon; |
||||
|
break; |
||||
|
case 'fieldset': |
||||
|
property.type = FormPropertyType.fieldset; |
||||
|
property.properties = form.items ? (form.items as JsonFormData[]).map(item => |
||||
|
jsonFormDataToProperty(item, level+1)).filter(p => p !== null) : []; |
||||
|
break; |
||||
|
} |
||||
|
if (!property.type) { |
||||
|
return null; |
||||
|
} |
||||
|
return property; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
Loading…
Reference in new issue