Browse Source

Introduce dynamic form properties settings, introduce dynamic form component. Migrate from json schema form to dynamic form.

pull/12325/head
Igor Kulikov 2 years ago
parent
commit
2c131f6767
  1. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  2. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  3. 6
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html
  4. 32
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts
  5. 143
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  6. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html
  7. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.scss
  8. 67
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts
  9. 173
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html
  10. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss
  11. 212
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts
  12. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html
  13. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.scss
  14. 95
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts
  15. 37
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html
  16. 23
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.scss
  17. 172
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts
  18. 61
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html
  19. 43
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.scss
  20. 204
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts
  21. 117
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html
  22. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.scss
  23. 265
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts
  24. 57
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html
  25. 85
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts
  26. 32
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models.ts
  27. 32
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  28. 15
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  29. 48
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  30. 2
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  31. 14
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts
  32. 4
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html
  33. 136
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts
  34. 43
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts
  35. 9
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  36. 28
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  37. 145
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts
  38. 2
      ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts
  39. 15
      ui-ngx/src/app/shared/components/value-input.component.html
  40. 11
      ui-ngx/src/app/shared/components/value-input.component.ts
  41. 1184
      ui-ngx/src/app/shared/models/ace/widget-completion.models.ts
  42. 385
      ui-ngx/src/app/shared/models/dynamic-form.models.ts
  43. 8
      ui-ngx/src/app/shared/models/widget.models.ts
  44. 80
      ui-ngx/src/assets/locale/locale.constant-en_US.json

3
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -30,6 +30,7 @@ import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models';
export interface AddWidgetDialogData { export interface AddWidgetDialogData {
dashboard: Dashboard; dashboard: Dashboard;
@ -108,6 +109,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction; const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
const actionSources = widgetInfo.actionSources; const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
let settingsForm = jsonFormSchemaToFormProperties(rawSettingsSchema);
let settingsSchema; let settingsSchema;
if (!rawSettingsSchema || rawSettingsSchema === '') { if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {}; settingsSchema = {};
@ -142,6 +144,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
typeParameters, typeParameters,
actionSources, actionSources,
isDataEnabled, isDataEnabled,
settingsForm,
settingsSchema, settingsSchema,
dataKeySettingsSchema, dataKeySettingsSchema,
latestDataKeySettingsSchema, latestDataKeySettingsSchema,

3
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts

@ -29,6 +29,7 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models';
@Component({ @Component({
selector: 'tb-edit-widget', selector: 'tb-edit-widget',
@ -146,6 +147,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction; const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
const actionSources = widgetInfo.actionSources; const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true; const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
const settingsForm = jsonFormSchemaToFormProperties(rawSettingsSchema);
let settingsSchema; let settingsSchema;
if (!rawSettingsSchema || rawSettingsSchema === '') { if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {}; settingsSchema = {};
@ -173,6 +175,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
typeParameters, typeParameters,
actionSources, actionSources,
isDataEnabled, isDataEnabled,
settingsForm,
settingsSchema, settingsSchema,
dataKeySettingsSchema, dataKeySettingsSchema,
latestDataKeySettingsSchema, latestDataKeySettingsSchema,

6
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html

@ -18,7 +18,11 @@
<div [formGroup]="widgetSettingsFormGroup"> <div [formGroup]="widgetSettingsFormGroup">
<ng-container #definedSettingsContent></ng-container> <ng-container #definedSettingsContent></ng-container>
<div class="tb-settings-directive-error" *ngIf="definedDirectiveError">{{definedDirectiveError}}</div> <div class="tb-settings-directive-error" *ngIf="definedDirectiveError">{{definedDirectiveError}}</div>
<tb-json-form *ngIf="useJsonForm()" #jsonFormComponent <tb-json-form *ngIf="useJsonForm()"
formControlName="settings"> formControlName="settings">
</tb-json-form> </tb-json-form>
<tb-dynamic-form *ngIf="useDynamicForm()"
[properties]="settingsForm"
formControlName="settings">
</tb-dynamic-form>
</div> </div>

32
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts

@ -40,13 +40,14 @@ import { Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; import { JsonFormComponent } from '@shared/components/json-form/json-form.component';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
import { IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models'; import { DynamicFormData, IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models';
import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
@Component({ @Component({
selector: 'tb-widget-settings', selector: 'tb-widget-settings',
@ -67,8 +68,6 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
@ViewChild('definedSettingsContent', {read: ViewContainerRef, static: true}) definedSettingsContainer: ViewContainerRef; @ViewChild('definedSettingsContent', {read: ViewContainerRef, static: true}) definedSettingsContainer: ViewContainerRef;
@ViewChild('jsonFormComponent') jsonFormComponent: JsonFormComponent;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -91,6 +90,8 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
definedDirectiveError: string; definedDirectiveError: string;
settingsForm?: FormProperty[];
widgetSettingsFormGroup: UntypedFormGroup; widgetSettingsFormGroup: UntypedFormGroup;
changeSubscription: Subscription; changeSubscription: Subscription;
@ -98,7 +99,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
private definedSettingsComponentRef: ComponentRef<IWidgetSettingsComponent>; private definedSettingsComponentRef: ComponentRef<IWidgetSettingsComponent>;
private definedSettingsComponent: IWidgetSettingsComponent; private definedSettingsComponent: IWidgetSettingsComponent;
private widgetSettingsFormData: JsonFormComponentData; private widgetSettingsFormData: JsonFormComponentData | DynamicFormData;
private propagateChange = (_v: any) => { }; private propagateChange = (_v: any) => { };
constructor(private translate: TranslateService, constructor(private translate: TranslateService,
@ -165,8 +166,13 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
} }
} }
writeValue(value: JsonFormComponentData): void { writeValue(value: JsonFormComponentData | DynamicFormData): void {
this.widgetSettingsFormData = value; this.widgetSettingsFormData = value;
if ('settingsForm' in this.widgetSettingsFormData) {
this.settingsForm = this.widgetSettingsFormData.settingsForm;
} else {
this.settingsForm = null;
}
if (this.changeSubscription) { if (this.changeSubscription) {
this.changeSubscription.unsubscribe(); this.changeSubscription.unsubscribe();
this.changeSubscription = null; this.changeSubscription = null;
@ -181,10 +187,12 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
this.updateModel(settings); this.updateModel(settings);
}); });
} else { } else {
this.widgetSettingsFormGroup.get('settings').patchValue(this.widgetSettingsFormData, {emitEvent: false}); const settingsValue = this.useJsonForm() ? this.widgetSettingsFormData : this.widgetSettingsFormData.model;
this.widgetSettingsFormGroup.get('settings').patchValue(settingsValue, {emitEvent: false});
this.changeSubscription = this.widgetSettingsFormGroup.get('settings').valueChanges.subscribe( this.changeSubscription = this.widgetSettingsFormGroup.get('settings').valueChanges.subscribe(
(widgetSettingsFormData: JsonFormComponentData) => { (data: JsonFormComponentData | WidgetSettings) => {
this.updateModel(widgetSettingsFormData.model); const settings = this.useJsonForm() ? data.model : data;
this.updateModel(settings);
} }
); );
} }
@ -196,7 +204,11 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
} }
useJsonForm(): boolean { useJsonForm(): boolean {
return !this.settingsDirective || !this.settingsDirective.length; return (!this.settingsDirective || !this.settingsDirective.length) && !this.settingsForm?.length;
}
useDynamicForm(): boolean {
return (!this.settingsDirective || !this.settingsDirective.length) && !!this.settingsForm?.length;
} }
private updateModel(settings: WidgetSettings) { private updateModel(settings: WidgetSettings) {
@ -246,7 +258,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
} }
}; };
} }
} else if (this.useJsonForm()) { } else if (this.useJsonForm() || this.useDynamicForm()) {
if (!this.widgetSettingsFormGroup.get('settings').valid) { if (!this.widgetSettingsFormGroup.get('settings').valid) {
return { return {
widgetSettings: { widgetSettings: {

143
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -65,6 +65,7 @@ import { catchError, map, take, takeUntil } from 'rxjs/operators';
import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
import { MatIconRegistry } from '@angular/material/icon'; import { MatIconRegistry } from '@angular/material/icon';
import { RafService } from '@core/services/raf.service'; import { RafService } from '@core/services/raf.service';
import { defaultFormPropertyValue, FormProperty, FormPropertyType } from '@shared/models/dynamic-form.models';
export interface ScadaSymbolApi { export interface ScadaSymbolApi {
generateElementId: () => string; generateElementId: () => string;
@ -151,60 +152,6 @@ export interface ScadaSymbolBehaviorAction extends ScadaSymbolBehaviorBase {
export type ScadaSymbolBehavior = ScadaSymbolBehaviorValue & ScadaSymbolBehaviorAction; export type ScadaSymbolBehavior = ScadaSymbolBehaviorValue & ScadaSymbolBehaviorAction;
export enum ScadaSymbolPropertyType {
text = 'text',
number = 'number',
switch = 'switch',
color = 'color',
color_settings = 'color_settings',
font = 'font',
units = 'units',
icon = 'icon'
}
export const scadaSymbolPropertyTypes = Object.keys(ScadaSymbolPropertyType) as ScadaSymbolPropertyType[];
export const scadaSymbolPropertyTypeTranslations = new Map<ScadaSymbolPropertyType, string>(
[
[ScadaSymbolPropertyType.text, 'scada.property.type-text'],
[ScadaSymbolPropertyType.number, 'scada.property.type-number'],
[ScadaSymbolPropertyType.switch, 'scada.property.type-switch'],
[ScadaSymbolPropertyType.color, 'scada.property.type-color'],
[ScadaSymbolPropertyType.color_settings, 'scada.property.type-color-settings'],
[ScadaSymbolPropertyType.font, 'scada.property.type-font'],
[ScadaSymbolPropertyType.units, 'scada.property.type-units'],
[ScadaSymbolPropertyType.icon, 'scada.property.type-icon']
]
);
export const scadaSymbolPropertyRowClasses =
['column', 'column-xs', 'column-lt-md', 'align-start', 'no-border', 'no-gap', 'no-padding', 'same-padding'];
export const scadaSymbolPropertyFieldClasses =
['medium-width', 'flex', 'flex-xs', 'flex-lt-md'];
export interface ScadaSymbolPropertyBase {
id: string;
name: string;
type: ScadaSymbolPropertyType;
default: any;
required?: boolean;
subLabel?: string;
divider?: boolean;
fieldSuffix?: string;
disableOnProperty?: string;
rowClass?: string;
fieldClass?: string;
}
export interface ScadaSymbolNumberProperty extends ScadaSymbolPropertyBase {
min?: number;
max?: number;
step?: number;
}
export type ScadaSymbolProperty = ScadaSymbolPropertyBase & ScadaSymbolNumberProperty;
export interface ScadaSymbolMetadata { export interface ScadaSymbolMetadata {
title: string; title: string;
description?: string; description?: string;
@ -215,7 +162,7 @@ export interface ScadaSymbolMetadata {
stateRender?: ScadaSymbolStateRenderFunction; stateRender?: ScadaSymbolStateRenderFunction;
tags: ScadaSymbolTag[]; tags: ScadaSymbolTag[];
behavior: ScadaSymbolBehavior[]; behavior: ScadaSymbolBehavior[];
properties: ScadaSymbolProperty[]; properties: FormProperty[];
} }
export const emptyMetadata = (width?: number, height?: number): ScadaSymbolMetadata => ({ export const emptyMetadata = (width?: number, height?: number): ScadaSymbolMetadata => ({
@ -507,7 +454,7 @@ export const defaultScadaSymbolObjectSettings = (metadata: ScadaSymbolMetadata):
} }
} }
for (const property of metadata.properties) { for (const property of metadata.properties) {
settings.properties[property.id] = property.default; settings.properties[property.id] = defaultFormPropertyValue(property);
} }
return settings; return settings;
}; };
@ -1020,33 +967,73 @@ export class ScadaSymbolObject {
return Array.isArray(element) ? element : [element]; return Array.isArray(element) ? element : [element];
} }
private getProperty(id: string): ScadaSymbolProperty { private getProperty(...ids: string[]): FormProperty {
return this.metadata.properties.find(p => p.id === id); let found: FormProperty;
let properties = this.metadata.properties;
for (const id of ids) {
if (properties) {
found = properties.find(p => p.id === id);
if (found && found.type === FormPropertyType.fieldset) {
properties = found.properties;
} else {
properties = null;
}
} else {
found = null;
}
}
return found;
}
private getSettingsValue(...ids: string[]): any {
let found: any;
let properties = this.settings.properties;
for (const id of ids) {
if (properties) {
found = properties[id];
if (found && typeof found === 'object') {
properties = found;
} else {
properties = null;
}
} else {
found = null;
}
}
return found;
} }
private getPropertyValue(id: string): any { private getPropertyValue(...ids: string[]): any {
const property = this.getProperty(id); const property = this.getProperty(...ids);
if (property) { if (property) {
const value = this.settings.properties[id]; if (property.type === FormPropertyType.fieldset) {
if (isDefinedAndNotNull(value)) { const propertyValue: {[id: string]: any} = {};
if (property.type === ScadaSymbolPropertyType.color_settings) { for (const childProperty of property.properties) {
return ColorProcessor.fromSettings(value); propertyValue[childProperty.id] = this.getPropertyValue(...ids, childProperty.id);
} else if (property.type === ScadaSymbolPropertyType.text) {
const result = this.ctx.utilsService.customTranslation(value, value);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
return createLabelFromSubscriptionEntityInfo(entityInfo, result);
} }
return value; return propertyValue;
} else { } else {
switch (property.type) { const value = this.getSettingsValue(...ids);
case ScadaSymbolPropertyType.text: if (isDefinedAndNotNull(value)) {
return ''; if (property.type === FormPropertyType.color_settings) {
case ScadaSymbolPropertyType.number: return ColorProcessor.fromSettings(value);
return 0; } else if (property.type === FormPropertyType.text) {
case ScadaSymbolPropertyType.color: const result = this.ctx.utilsService.customTranslation(value, value);
return '#000'; const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
case ScadaSymbolPropertyType.color_settings: return createLabelFromSubscriptionEntityInfo(entityInfo, result);
return ColorProcessor.fromSettings(constantColor('#000')); }
return value;
} else {
switch (property.type) {
case FormPropertyType.text:
return '';
case FormPropertyType.number:
return 0;
case FormPropertyType.color:
return '#000';
case FormPropertyType.color_settings:
return ColorProcessor.fromSettings(constantColor('#000'));
}
} }
} }
} else { } else {

26
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html

@ -15,12 +15,12 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-form-panel tb-scada-symbol-properties"> <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"> <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" [class.no-padding-right]="!disabled">
<div class="tb-form-table-header-cell tb-id-header" translate>scada.property.id</div> <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>scada.property.name</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>scada.property.type</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" [class.disabled]="disabled"></div> <div class="tb-form-table-header-cell tb-actions-header" [class.disabled]="disabled"></div>
</div> </div>
<div *ngIf="propertiesFormArray().controls.length; else noProperties" class="tb-form-table-body tb-drop-list" <div *ngIf="propertiesFormArray().controls.length; else noProperties" class="tb-form-table-body tb-drop-list"
@ -30,12 +30,12 @@
<div cdkDrag [cdkDragDisabled]="!dragEnabled" <div cdkDrag [cdkDragDisabled]="!dragEnabled"
[class.tb-draggable-form-table-row]="!disabled" [class.tb-draggable-form-table-row]="!disabled"
*ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;"> *ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;">
<tb-scada-symbol-metadata-property-row class="flex-1" <tb-dynamic-form-property-row class="flex-1"
[index]="$index" [index]="$index"
[booleanPropertyIds]="booleanPropertyIds" [booleanPropertyIds]="booleanPropertyIds"
[formControl]="propertyControl" [formControl]="propertyControl"
(propertyRemoved)="removeProperty($index)"> (propertyRemoved)="removeProperty($index)">
</tb-scada-symbol-metadata-property-row> </tb-dynamic-form-property-row>
<div class="tb-form-table-row-cell-buttons"> <div class="tb-form-table-row-cell-buttons">
<button mat-icon-button <button mat-icon-button
*ngIf="!disabled" *ngIf="!disabled"
@ -53,11 +53,11 @@
</div> </div>
<div *ngIf="!disabled"> <div *ngIf="!disabled">
<button type="button" mat-stroked-button color="primary" (click)="addProperty()"> <button type="button" mat-stroked-button color="primary" (click)="addProperty()">
{{ 'scada.property.add-property' | translate }} {{ 'dynamic-form.property.add-property' | translate }}
</button> </button>
</div> </div>
</div> </div>
<ng-template #noProperties> <ng-template #noProperties>
<span class="tb-prompt flex items-center justify-center">{{ 'scada.property.no-properties' | translate }}</span> <span class="tb-prompt flex items-center justify-center" [style]="fillHeight ? {height: '100%'} : {}">{{ 'dynamic-form.property.no-properties' | translate }}</span>
</ng-template> </ng-template>

8
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.scss

@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
.tb-scada-symbol-properties { .tb-dynamic-form-properties {
flex: 1; flex: 1;
margin: 12px; &:not(.no-margin) {
margin: 12px;
}
.tb-form-table-header-cell { .tb-form-table-header-cell {
&.tb-id-header { &.tb-id-header {
flex: 1 1 40%; flex: 1 1 40%;
@ -40,7 +42,7 @@
} }
.tb-form-table-body { .tb-form-table-body {
overflow: auto; overflow: auto;
tb-scada-symbol-metadata-property-row { tb-dynamic-form-property-row {
overflow: hidden; overflow: hidden;
} }
} }

67
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts

@ -15,7 +15,7 @@
/// ///
import { import {
Component, Component, DestroyRef,
forwardRef, forwardRef,
HostBinding, HostBinding,
Input, Input,
@ -35,43 +35,61 @@ import {
UntypedFormGroup, UntypedFormGroup,
Validator Validator
} from '@angular/forms'; } from '@angular/forms';
import { ScadaSymbolProperty, ScadaSymbolPropertyType } from '@home/components/widget/lib/scada/scada-symbol.models';
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop } from '@angular/cdk/drag-drop';
import {
propertyValid,
ScadaSymbolPropertyRowComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { FormProperty, FormPropertyType } from '@shared/models/dynamic-form.models';
import {
DynamicFormPropertyRowComponent,
propertyValid
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component';
import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'tb-scada-symbol-metadata-properties', selector: 'tb-dynamic-form-properties',
templateUrl: './scada-symbol-properties.component.html', templateUrl: './dynamic-form-properties.component.html',
styleUrls: ['./scada-symbol-properties.component.scss'], styleUrls: ['./dynamic-form-properties.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaSymbolPropertiesComponent), useExisting: forwardRef(() => DynamicFormPropertiesComponent),
multi: true multi: true
}, },
{ {
provide: NG_VALIDATORS, provide: NG_VALIDATORS,
useExisting: forwardRef(() => ScadaSymbolPropertiesComponent), useExisting: forwardRef(() => DynamicFormPropertiesComponent),
multi: true multi: true
} }
], ],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnInit, Validator { export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnInit, Validator {
@HostBinding('style.display') styleDisplay = 'flex'; @HostBinding('style.display') styleDisplay = 'flex';
@HostBinding('style.overflow') styleOverflow = 'hidden'; @HostBinding('style.overflow') styleOverflow = 'hidden';
@HostBinding('style.height')
get containerHeight(): string {
return this.fillHeight ? '100%': 'auto';
}
@ViewChildren(ScadaSymbolPropertyRowComponent) @ViewChildren(DynamicFormPropertyRowComponent)
propertyRows: QueryList<ScadaSymbolPropertyRowComponent>; propertyRows: QueryList<DynamicFormPropertyRowComponent>;
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
@coerceBoolean()
noBorder = false;
@Input()
@coerceBoolean()
noMargin = false;
@Input()
@coerceBoolean()
fillHeight = false;
booleanPropertyIds: string[] = []; booleanPropertyIds: string[] = [];
propertiesFormGroup: UntypedFormGroup; propertiesFormGroup: UntypedFormGroup;
@ -85,6 +103,7 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef,
private translate: TranslateService) { private translate: TranslateService) {
} }
@ -92,13 +111,15 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
this.propertiesFormGroup = this.fb.group({ this.propertiesFormGroup = this.fb.group({
properties: this.fb.array([]) properties: this.fb.array([])
}); });
this.propertiesFormGroup.valueChanges.subscribe( this.propertiesFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => { () => {
let properties: ScadaSymbolProperty[] = this.propertiesFormGroup.get('properties').value; let properties: FormProperty[] = this.propertiesFormGroup.get('properties').value;
if (properties) { if (properties) {
properties = properties.filter(p => propertyValid(p)); properties = properties.filter(p => propertyValid(p));
} }
this.booleanPropertyIds = properties.filter(p => p.type === ScadaSymbolPropertyType.switch).map(p => p.id); this.booleanPropertyIds = properties.filter(p => p.type === FormPropertyType.switch).map(p => p.id);
properties.forEach((p, i) => { properties.forEach((p, i) => {
if (p.disableOnProperty && !this.booleanPropertyIds.includes(p.disableOnProperty)) { if (p.disableOnProperty && !this.booleanPropertyIds.includes(p.disableOnProperty)) {
p.disableOnProperty = null; p.disableOnProperty = null;
@ -127,10 +148,10 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
} }
} }
writeValue(value: ScadaSymbolProperty[] | undefined): void { writeValue(value: FormProperty[] | undefined): void {
const properties= value || []; const properties= value || [];
this.propertiesFormGroup.setControl('properties', this.preparePropertiesFormArray(properties), {emitEvent: false}); this.propertiesFormGroup.setControl('properties', this.preparePropertiesFormArray(properties), {emitEvent: false});
this.booleanPropertyIds = properties.filter(p => p.type === ScadaSymbolPropertyType.switch).map(p => p.id); this.booleanPropertyIds = properties.filter(p => p.type === FormPropertyType.switch).map(p => p.id);
} }
public validate(c: UntypedFormControl) { public validate(c: UntypedFormControl) {
@ -141,7 +162,7 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
for (const control of notUniqueControls) { for (const control of notUniqueControls) {
control.updateValueAndValidity({onlySelf: false, emitEvent: false}); control.updateValueAndValidity({onlySelf: false, emitEvent: false});
if (control.hasError('propertyIdNotUnique')) { if (control.hasError('propertyIdNotUnique')) {
this.errorText = this.translate.instant('scada.property.not-unique-property-ids-error'); this.errorText = this.translate.instant('dynamic-form.property.not-unique-property-ids-error');
} }
} }
const valid = this.propertiesFormGroup.valid; const valid = this.propertiesFormGroup.valid;
@ -185,10 +206,10 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
} }
addProperty() { addProperty() {
const property: ScadaSymbolProperty = { const property: FormProperty = {
id: '', id: '',
name: '', name: '',
type: ScadaSymbolPropertyType.text, type: FormPropertyType.text,
default: '' default: ''
}; };
const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray;
@ -202,7 +223,7 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
}); });
} }
private preparePropertiesFormArray(properties: ScadaSymbolProperty[] | undefined): UntypedFormArray { private preparePropertiesFormArray(properties: FormProperty[] | undefined): UntypedFormArray {
const propertiesControls: Array<AbstractControl> = []; const propertiesControls: Array<AbstractControl> = [];
if (properties) { if (properties) {
properties.forEach((property) => { properties.forEach((property) => {

173
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html

@ -15,48 +15,110 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-scada-symbol-property-settings-panel" [formGroup]="propertyFormGroup"> <div class="tb-dynamic-form-property-settings-panel" [formGroup]="propertyFormGroup">
<div class="tb-scada-symbol-property-settings-title">{{ panelTitle | translate }}</div> <div class="tb-dynamic-form-property-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-scada-symbol-property-settings-panel-content"> <div class="tb-dynamic-form-property-settings-panel-content">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.id</div> <div class="fixed-title-width" translate>dynamic-form.property.id</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}"> <input required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.name</div> <div class="fixed-title-width" translate>dynamic-form.property.name</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}"> <input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.type</div> <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-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type"> <mat-select formControlName="type">
<mat-option *ngFor="let type of scadaSymbolPropertyTypes" [value]="type"> <mat-option *ngFor="let type of formPropertyTypes" [value]="type">
{{ scadaSymbolPropertyTypeTranslations.get(type) | translate }} {{ formPropertyTypeTranslations.get(type) | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row space-between"> <div *ngIf="propertyFormGroup.get('type').value === FormPropertyType.number" class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.default-value</div> <div translate>dynamic-form.property.number-settings</div>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.text" <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="propertyFormGroup.get('type').value === 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>
<ng-container *ngIf="propertyFormGroup.get('type').value === FormPropertyType.select">
<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>
{{ 'dynamic-form.property.select-options' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div 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>
<tb-dynamic-form-select-items
formControlName="items">
</tb-dynamic-form-select-items>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container>
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.default-value</div>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === FormPropertyType.text"
class="flex" class="flex"
appearance="outline" subscriptSizing="dynamic"> appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="default" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="default" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<tb-color-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.color" <tb-color-input *ngIf="propertyFormGroup.get('type').value === FormPropertyType.color"
asBoxInput asBoxInput
colorClearButton colorClearButton
formControlName="default"> formControlName="default">
</tb-color-input> </tb-color-input>
<tb-color-settings *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.color_settings" <tb-color-settings *ngIf="propertyFormGroup.get('type').value === FormPropertyType.color_settings"
formControlName="default" formControlName="default"
settingsKey="{{ propertyFormGroup.get('id').value }}"> settingsKey="{{ propertyFormGroup.get('id').value }}">
</tb-color-settings> </tb-color-settings>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.number" <mat-form-field *ngIf="propertyFormGroup.get('type').value === FormPropertyType.number"
appearance="outline" appearance="outline"
class="number" class="number"
subscriptSizing="dynamic"> subscriptSizing="dynamic">
@ -66,78 +128,69 @@
[step]="propertyFormGroup.get('step').value" [step]="propertyFormGroup.get('step').value"
type="number" placeholder="{{ 'widget-config.set' | translate }}"> type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<tb-font-settings *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.font" <tb-font-settings *ngIf="propertyFormGroup.get('type').value === FormPropertyType.font"
formControlName="default" formControlName="default"
clearButton clearButton
disabledLineHeight> disabledLineHeight>
</tb-font-settings> </tb-font-settings>
<tb-unit-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.units" <tb-unit-input *ngIf="propertyFormGroup.get('type').value === FormPropertyType.units"
formControlName="default"></tb-unit-input> formControlName="default"></tb-unit-input>
<tb-value-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.switch" <tb-value-input *ngIf="propertyFormGroup.get('type').value === FormPropertyType.switch"
[valueType]="ValueType.BOOLEAN" [valueType]="ValueType.BOOLEAN"
formControlName="default"></tb-value-input> formControlName="default"></tb-value-input>
<tb-material-icon-select *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.icon" <tb-material-icon-select *ngIf="propertyFormGroup.get('type').value === FormPropertyType.icon"
asBoxInput asBoxInput
formControlName="default"> formControlName="default">
</tb-material-icon-select> </tb-material-icon-select>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === FormPropertyType.select" class="medium-width"
appearance="outline" subscriptSizing="dynamic">
<mat-select *ngIf="!propertyFormGroup.get('multiple').value" formControlName="default">
<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>
</div> </div>
<div *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.number" class="tb-form-row space-between"> <div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" 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"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="required">
{{ 'scada.property.value-required' | translate }} {{ 'dynamic-form.property.value-required' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings"> <mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header class="flex flex-row flex-wrap"> <mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title> <mat-panel-title>
{{ 'scada.property.advanced-ui-settings' | translate }} {{ 'dynamic-form.property.advanced-ui-settings' | translate }}
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div class="tb-form-row space-between"> <div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.sub-label</div> <div class="fixed-title-width" translate>dynamic-form.property.sub-label</div>
<mat-form-field class="flex" <mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic"> appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="subLabel" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="subLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row space-between"> <div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="divider"> <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="divider">
{{ 'scada.property.vertical-divider-after' | translate }} {{ 'dynamic-form.property.vertical-divider-after' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div *ngIf="[ScadaSymbolPropertyType.number, ScadaSymbolPropertyType.text].includes(propertyFormGroup.get('type').value)" <div *ngIf="[FormPropertyType.number, FormPropertyType.text].includes(propertyFormGroup.get('type').value)"
class="tb-form-row space-between"> class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.input-field-suffix</div> <div class="fixed-title-width" translate>dynamic-form.property.input-field-suffix</div>
<mat-form-field class="flex" <mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic"> appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="fieldSuffix" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="fieldSuffix" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.disable-on-property</div> <div class="fixed-title-width" translate>dynamic-form.property.disable-on-property</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="disableOnProperty" placeholder="{{ 'widget-config.set' | translate }}"> <mat-select formControlName="disableOnProperty" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
@ -148,20 +201,28 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.property-row-classes</div> <tb-js-func
class="flex-1"
formControlName="condition"
[functionArgs]="['property', 'model']"
functionTitle="{{ 'dynamic-form.property.display-condition-function' | translate }}">
</tb-js-func>
</div>
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.property-row-classes</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="rowClass" multiple placeholder="{{ 'widget-config.set' | translate }}"> <mat-select formControlName="rowClass" multiple placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let clazz of scadaSymbolPropertyRowClasses" [value]="clazz"> <mat-option *ngFor="let clazz of formPropertyRowClasses" [value]="clazz">
{{ clazz }} {{ clazz }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="propertyFormGroup.get('type').value !== ScadaSymbolPropertyType.switch" class="tb-form-row"> <div *ngIf="![FormPropertyType.fieldset, FormPropertyType.switch].includes(propertyFormGroup.get('type').value)" class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.property-field-classes</div> <div class="fixed-title-width" translate>dynamic-form.property.property-field-classes</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="fieldClass" multiple placeholder="{{ 'widget-config.set' | translate }}"> <mat-select formControlName="fieldClass" multiple placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let clazz of scadaSymbolPropertyFieldClasses" [value]="clazz"> <mat-option *ngFor="let clazz of formPropertyFieldClasses" [value]="clazz">
{{ clazz }} {{ clazz }}
</mat-option> </mat-option>
</mat-select> </mat-select>
@ -171,7 +232,7 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
</div> </div>
<div class="tb-scada-symbol-property-settings-panel-buttons"> <div class="tb-dynamic-form-property-settings-panel-buttons">
<button mat-button <button mat-button
color="primary" color="primary"
type="button" type="button"

10
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../../scss/constants'; @import '../../../../../../../../../scss/constants';
.tb-scada-symbol-property-settings-panel { .tb-dynamic-form-property-settings-panel {
width: 540px; width: 540px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -23,7 +23,7 @@
@media #{$mat-lt-md} { @media #{$mat-lt-md} {
width: 90vw; width: 90vw;
} }
.tb-scada-symbol-property-settings-panel-content { .tb-dynamic-form-property-settings-panel-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
@ -31,14 +31,14 @@
margin: -10px; margin: -10px;
padding: 10px; padding: 10px;
} }
.tb-scada-symbol-property-settings-title { .tb-dynamic-form-property-settings-title {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
line-height: 24px; line-height: 24px;
letter-spacing: 0.25px; letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
.tb-scada-symbol-property-settings-panel-buttons { .tb-dynamic-form-property-settings-panel-buttons {
height: 40px; height: 40px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;

212
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts

@ -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});
});
}
}
}
}
}

10
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div [formGroup]="propertyRowFormGroup" class="tb-form-table-row tb-scada-symbol-metadata-property-row"> <div [formGroup]="propertyRowFormGroup" class="tb-form-table-row tb-dynamic-form-property-row">
<mat-form-field class="tb-inline-field tb-id-field" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-inline-field tb-id-field" appearance="outline" subscriptSizing="dynamic">
<input #idInput required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}"> <input #idInput required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
@ -24,8 +24,8 @@
</mat-form-field> </mat-form-field>
<mat-form-field class="tb-inline-field tb-type-field fixed-height" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-inline-field tb-type-field fixed-height" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type"> <mat-select formControlName="type">
<mat-option *ngFor="let type of scadaSymbolPropertyTypes" [value]="type"> <mat-option *ngFor="let type of formPropertyTypes" [value]="type">
{{ scadaSymbolPropertyTypeTranslations.get(type) | translate }} {{ formPropertyTypeTranslations.get(type) | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -34,7 +34,7 @@
mat-icon-button mat-icon-button
#editButton #editButton
(click)="editProperty($event, editButton)" (click)="editProperty($event, editButton)"
matTooltip="{{ 'scada.property.property-settings' | translate }}" matTooltip="{{ 'dynamic-form.property.property-settings' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
</button> </button>
@ -42,7 +42,7 @@
type="button" type="button"
mat-icon-button mat-icon-button
(click)="propertyRemoved.emit()" (click)="propertyRemoved.emit()"
matTooltip="{{ 'scada.property.remove-property' | translate }}" matTooltip="{{ 'dynamic-form.property.remove-property' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>

2
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.scss

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
.tb-scada-symbol-metadata-property-row { .tb-dynamic-form-property-row {
.tb-id-field { .tb-id-field {
flex: 1 1 40%; flex: 1 1 40%;
} }

95
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts

@ -16,7 +16,7 @@
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component, DestroyRef,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
forwardRef, forwardRef,
@ -39,38 +39,39 @@ import {
ValidatorFn, ValidatorFn,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import {
ScadaSymbolProperty,
ScadaSymbolPropertyType,
scadaSymbolPropertyTypes,
scadaSymbolPropertyTypeTranslations
} from '@home/components/widget/lib/scada/scada-symbol.models';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { constantColor, Font } from '@shared/models/widget-settings.models'; import { constantColor, Font } from '@shared/models/widget-settings.models';
import { import {
ScadaSymbolPropertyPanelComponent FormProperty,
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component'; FormPropertyType,
formPropertyTypes,
formPropertyTypeTranslations
} from '@shared/models/dynamic-form.models';
import {
DynamicFormPropertiesComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component';
import { import {
ScadaSymbolPropertiesComponent DynamicFormPropertyPanelComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-properties.component'; } from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export const propertyValid = (property: ScadaSymbolProperty): boolean => !(!property.id || !property.name || !property.type); export const propertyValid = (property: FormProperty): boolean => !(!property.id || !property.name || !property.type);
export const defaultPropertyValue = (type: ScadaSymbolPropertyType): any => { export const defaultPropertyValue = (type: FormPropertyType): any => {
switch (type) { switch (type) {
case ScadaSymbolPropertyType.text: case FormPropertyType.text:
return ''; return '';
case ScadaSymbolPropertyType.number: case FormPropertyType.number:
return 0; return 0;
case ScadaSymbolPropertyType.switch: case FormPropertyType.switch:
return false; return false;
case ScadaSymbolPropertyType.color: case FormPropertyType.color:
return '#000'; return '#000';
case ScadaSymbolPropertyType.color_settings: case FormPropertyType.color_settings:
return constantColor('#000'); return constantColor('#000');
case ScadaSymbolPropertyType.font: case FormPropertyType.font:
return { return {
size: 12, size: 12,
sizeUnit: 'px', sizeUnit: 'px',
@ -79,32 +80,35 @@ export const defaultPropertyValue = (type: ScadaSymbolPropertyType): any => {
style: 'normal', style: 'normal',
lineHeight: '1' lineHeight: '1'
} as Font; } as Font;
case ScadaSymbolPropertyType.units: case FormPropertyType.units:
return ''; return '';
case ScadaSymbolPropertyType.icon: case FormPropertyType.icon:
return 'star'; return 'star';
case FormPropertyType.fieldset:
case FormPropertyType.select:
return null;
} }
}; };
@Component({ @Component({
selector: 'tb-scada-symbol-metadata-property-row', selector: 'tb-dynamic-form-property-row',
templateUrl: './scada-symbol-property-row.component.html', templateUrl: './dynamic-form-property-row.component.html',
styleUrls: ['./scada-symbol-property-row.component.scss'], styleUrls: ['./dynamic-form-property-row.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent), useExisting: forwardRef(() => DynamicFormPropertyRowComponent),
multi: true multi: true
}, },
{ {
provide: NG_VALIDATORS, provide: NG_VALIDATORS,
useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent), useExisting: forwardRef(() => DynamicFormPropertyRowComponent),
multi: true multi: true
} }
], ],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, OnInit, Validator { export class DynamicFormPropertyRowComponent implements ControlValueAccessor, OnInit, Validator {
@ViewChild('idInput') @ViewChild('idInput')
idInput: ElementRef<HTMLInputElement>; idInput: ElementRef<HTMLInputElement>;
@ -112,8 +116,8 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
@ViewChild('editButton') @ViewChild('editButton')
editButton: MatButton; editButton: MatButton;
scadaSymbolPropertyTypes = scadaSymbolPropertyTypes; formPropertyTypes = formPropertyTypes;
scadaSymbolPropertyTypeTranslations = scadaSymbolPropertyTypeTranslations; formPropertyTypeTranslations = formPropertyTypeTranslations;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -129,16 +133,17 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
propertyRowFormGroup: UntypedFormGroup; propertyRowFormGroup: UntypedFormGroup;
modelValue: ScadaSymbolProperty; modelValue: FormProperty;
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private propertiesComponent: ScadaSymbolPropertiesComponent) { private propertiesComponent: DynamicFormPropertiesComponent) {
} }
ngOnInit() { ngOnInit() {
@ -147,10 +152,14 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
name: [null, [Validators.required]], name: [null, [Validators.required]],
type: [null, [Validators.required]] type: [null, [Validators.required]]
}); });
this.propertyRowFormGroup.valueChanges.subscribe( this.propertyRowFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => this.updateModel() () => this.updateModel()
); );
this.propertyRowFormGroup.get('type').valueChanges.subscribe((newType: ScadaSymbolPropertyType) => { this.propertyRowFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((newType: FormPropertyType) => {
this.onTypeChanged(newType); this.onTypeChanged(newType);
}); });
} }
@ -171,7 +180,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
} }
} }
writeValue(value: ScadaSymbolProperty): void { writeValue(value: FormProperty): void {
this.modelValue = value; this.modelValue = value;
this.propertyRowFormGroup.patchValue( this.propertyRowFormGroup.patchValue(
{ {
@ -197,14 +206,14 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
booleanPropertyIds: this.booleanPropertyIds, booleanPropertyIds: this.booleanPropertyIds,
property: deepClone(this.modelValue) property: deepClone(this.modelValue)
}; };
const scadaSymbolPropertyPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, const dynamicFormPropertyPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ScadaSymbolPropertyPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, this.viewContainerRef, DynamicFormPropertyPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx, ctx,
{}, {},
{}, {}, true); {}, {}, true);
scadaSymbolPropertyPanelPopover.tbComponentRef.instance.popover = scadaSymbolPropertyPanelPopover; dynamicFormPropertyPanelPopover.tbComponentRef.instance.popover = dynamicFormPropertyPanelPopover;
scadaSymbolPropertyPanelPopover.tbComponentRef.instance.propertySettingsApplied.subscribe((property) => { dynamicFormPropertyPanelPopover.tbComponentRef.instance.propertySettingsApplied.subscribe((property) => {
scadaSymbolPropertyPanelPopover.hide(); dynamicFormPropertyPanelPopover.hide();
this.propertyRowFormGroup.patchValue( this.propertyRowFormGroup.patchValue(
{ {
id: property.id, id: property.id,
@ -215,7 +224,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
this.modelValue = property; this.modelValue = property;
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
}); });
scadaSymbolPropertyPanelPopover.tbDestroy.subscribe(() => { dynamicFormPropertyPanelPopover.tbDestroy.subscribe(() => {
if (!propertyValid(this.modelValue)) { if (!propertyValid(this.modelValue)) {
editCanceled(); editCanceled();
} }
@ -244,7 +253,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
propertyIdNotUnique: true propertyIdNotUnique: true
}; };
} }
const property: ScadaSymbolProperty = {...this.modelValue, ...this.propertyRowFormGroup.value}; const property: FormProperty = {...this.modelValue, ...this.propertyRowFormGroup.value};
if (!propertyValid(property)) { if (!propertyValid(property)) {
return { return {
property: true property: true
@ -269,13 +278,13 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
}; };
} }
private onTypeChanged(newType: ScadaSymbolPropertyType) { private onTypeChanged(newType: FormPropertyType) {
this.modelValue = {...this.modelValue, ...{type: newType}}; this.modelValue = {...this.modelValue, ...{type: newType}};
this.modelValue.default = defaultPropertyValue(newType); this.modelValue.default = defaultPropertyValue(newType);
} }
private updateModel() { private updateModel() {
const value: ScadaSymbolProperty = this.propertyRowFormGroup.value; const value: FormProperty = this.propertyRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value}; this.modelValue = {...this.modelValue, ...value};
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
} }

37
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html

@ -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>

23
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.scss

@ -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%;
}
}

172
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts

@ -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);
}
}

61
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html

@ -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>

43
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.scss

@ -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;
}
}
}

204
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts

@ -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);
}
}

117
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html

@ -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>

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.scss

@ -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;
}
}

265
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts

@ -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);
}
}

57
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html

@ -36,58 +36,11 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
</div> </div>
<div [class.!hidden]="!propertyRows?.length && !appearanceProperties.children.length" class="tb-form-panel" formGroupName="properties"> <tb-dynamic-form formControlName="properties"
<div class="tb-form-panel-title" translate>widget-config.appearance</div> title="{{ 'widget-config.appearance' | translate }}"
<div #appearanceProperties><ng-content select=".tb-scada-symbol-appearance-properties"></ng-content></div> [properties]="metadata?.properties">
<div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between flex-wrap overflow-auto" [class]="propertyRow.rowClass"> <ng-container class="tb-properties-content"><ng-content select=".tb-scada-symbol-appearance-properties"></ng-content></ng-container>
<mat-slide-toggle *ngIf="propertyRow.switch" class="mat-slide fixed-title-width margin" formControlName="{{ propertyRow.switch.id }}"> </tb-dynamic-form>
{{ 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">
<div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel | customTranslate }}</div>
<mat-form-field *ngIf="property.type === ScadaSymbolPropertyType.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>
<tb-color-input *ngIf="property.type === ScadaSymbolPropertyType.color"
[required]="property.required"
[class]="property.fieldClass"
asBoxInput
colorClearButton
formControlName="{{ property.id }}">
</tb-color-input>
<tb-color-settings *ngIf="property.type === ScadaSymbolPropertyType.color_settings"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
settingsKey="{{ property.name | customTranslate }}">
</tb-color-settings>
<mat-form-field *ngIf="property.type === ScadaSymbolPropertyType.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 === ScadaSymbolPropertyType.font"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
clearButton
disabledLineHeight>
</tb-font-settings>
<tb-unit-input *ngIf="property.type === ScadaSymbolPropertyType.units"
[class]="property.fieldClass"
[required]="property.required"
formControlName="{{ property.id }}"></tb-unit-input>
<tb-material-icon-select *ngIf="property.type === ScadaSymbolPropertyType.icon"
asBoxInput
formControlName="{{ property.id }}">
</tb-material-icon-select>
<mat-divider *ngIf="property.divider" vertical></mat-divider>
</ng-container>
</div>
</div>
</div>
</ng-container> </ng-container>
<ng-template #behaviorsTpl let-behaviors="behaviors"> <ng-template #behaviorsTpl let-behaviors="behaviors">

85
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts

@ -22,9 +22,7 @@ import {
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
Validator, Validator
ValidatorFn,
Validators
} from '@angular/forms'; } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -33,19 +31,16 @@ import {
parseScadaSymbolMetadataFromContent, parseScadaSymbolMetadataFromContent,
ScadaSymbolBehaviorType, ScadaSymbolBehaviorType,
ScadaSymbolMetadata, ScadaSymbolMetadata,
ScadaSymbolObjectSettings, ScadaSymbolObjectSettings
ScadaSymbolPropertyType
} from '@home/components/widget/lib/scada/scada-symbol.models'; } from '@home/components/widget/lib/scada/scada-symbol.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models'; import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; import { mergeDeep } from '@core/utils';
import { import {
ScadaSymbolBehaviorGroup, ScadaSymbolBehaviorGroup,
ScadaSymbolPropertyRow, toBehaviorGroups
toBehaviorGroups,
toPropertyRows
} from '@home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models'; } from '@home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models';
import { merge, Observable, of, Subscription } from 'rxjs'; import { Observable, of } from 'rxjs';
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models'; import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models';
import { ImageService } from '@core/http/image.service'; import { ImageService } from '@core/http/image.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -71,8 +66,6 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
ScadaSymbolBehaviorType = ScadaSymbolBehaviorType; ScadaSymbolBehaviorType = ScadaSymbolBehaviorType;
ScadaSymbolPropertyType = ScadaSymbolPropertyType;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -101,14 +94,10 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
private propagateChange = null; private propagateChange = null;
private validatorTriggers: string[];
private validatorSubscription: Subscription;
public scadaSymbolObjectSettingsFormGroup: UntypedFormGroup; public scadaSymbolObjectSettingsFormGroup: UntypedFormGroup;
metadata: ScadaSymbolMetadata; metadata: ScadaSymbolMetadata;
behaviorGroups: ScadaSymbolBehaviorGroup[]; behaviorGroups: ScadaSymbolBehaviorGroup[];
propertyRows: ScadaSymbolPropertyRow[];
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
@ -119,7 +108,7 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
ngOnInit(): void { ngOnInit(): void {
this.scadaSymbolObjectSettingsFormGroup = this.fb.group({ this.scadaSymbolObjectSettingsFormGroup = this.fb.group({
behavior: this.fb.group({}), behavior: this.fb.group({}),
properties: this.fb.group({}) properties: [{}, []]
}); });
this.scadaSymbolObjectSettingsFormGroup.valueChanges.subscribe(() => { this.scadaSymbolObjectSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
@ -151,7 +140,6 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
this.scadaSymbolObjectSettingsFormGroup.disable({emitEvent: false}); this.scadaSymbolObjectSettingsFormGroup.disable({emitEvent: false});
} else { } else {
this.scadaSymbolObjectSettingsFormGroup.enable({emitEvent: false}); this.scadaSymbolObjectSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
} }
} }
@ -170,12 +158,6 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
} }
private loadMetadata() { private loadMetadata() {
if (this.validatorSubscription) {
this.validatorSubscription.unsubscribe();
this.validatorSubscription = null;
}
this.validatorTriggers = [];
let metadata$: Observable<ScadaSymbolMetadata>; let metadata$: Observable<ScadaSymbolMetadata>;
if (this.scadaSymbolMetadata) { if (this.scadaSymbolMetadata) {
metadata$ = of(this.scadaSymbolMetadata); metadata$ = of(this.scadaSymbolMetadata);
@ -196,74 +178,19 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
(metadata) => { (metadata) => {
this.metadata = metadata; this.metadata = metadata;
this.behaviorGroups = toBehaviorGroups(this.metadata.behavior); this.behaviorGroups = toBehaviorGroups(this.metadata.behavior);
this.propertyRows = toPropertyRows(this.metadata.properties);
const behaviorFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('behavior') as UntypedFormGroup; const behaviorFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('behavior') as UntypedFormGroup;
for (const control of Object.keys(behaviorFormGroup.controls)) { for (const control of Object.keys(behaviorFormGroup.controls)) {
behaviorFormGroup.removeControl(control, {emitEvent: false}); behaviorFormGroup.removeControl(control, {emitEvent: false});
} }
const propertiesFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('properties') as UntypedFormGroup;
for (const control of Object.keys(propertiesFormGroup.controls)) {
propertiesFormGroup.removeControl(control, {emitEvent: false});
}
for (const behavior of this.metadata.behavior) { for (const behavior of this.metadata.behavior) {
behaviorFormGroup.addControl(behavior.id, this.fb.control(null, []), {emitEvent: false}); behaviorFormGroup.addControl(behavior.id, this.fb.control(null, []), {emitEvent: false});
} }
for (const property of this.metadata.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 === ScadaSymbolPropertyType.number) {
if (isDefinedAndNotNull(property.min)) {
validators.push(Validators.min(property.min));
}
if (isDefinedAndNotNull(property.max)) {
validators.push(Validators.max(property.max));
}
}
propertiesFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false});
}
if (this.validatorTriggers.length) {
const observables: Observable<any>[] = [];
for (const trigger of this.validatorTriggers) {
if (propertiesFormGroup.get(trigger)) {
observables.push(propertiesFormGroup.get(trigger).valueChanges);
}
}
if (observables.length) {
this.validatorSubscription = merge(...observables).subscribe(() => {
this.updateValidators();
});
}
}
this.setupValue(); this.setupValue();
this.cd.markForCheck(); this.cd.markForCheck();
} }
); );
} }
private updateValidators() {
const propertiesFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('properties') as UntypedFormGroup;
for (const trigger of this.validatorTriggers) {
const value: boolean = propertiesFormGroup.get(trigger).value;
this.metadata.properties.filter(p => p.disableOnProperty === trigger).forEach(
(p) => {
const control = propertiesFormGroup.get(p.id);
if (value) {
control.enable({emitEvent: false});
} else {
control.disable({emitEvent: false});
}
}
);
}
}
private setupValue() { private setupValue() {
if (this.metadata) { if (this.metadata) {
const defaults = defaultScadaSymbolObjectSettings(this.metadata); const defaults = defaultScadaSymbolObjectSettings(this.metadata);

32
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models.ts

@ -15,9 +15,7 @@
/// ///
import { import {
ScadaSymbolBehavior, ScadaSymbolBehavior
ScadaSymbolProperty,
ScadaSymbolPropertyType
} from '@home/components/widget/lib/scada/scada-symbol.models'; } from '@home/components/widget/lib/scada/scada-symbol.models';
export interface ScadaSymbolBehaviorGroup { export interface ScadaSymbolBehaviorGroup {
@ -25,13 +23,6 @@ export interface ScadaSymbolBehaviorGroup {
behaviors: ScadaSymbolBehavior[]; behaviors: ScadaSymbolBehavior[];
} }
export interface ScadaSymbolPropertyRow {
label: string;
properties: ScadaSymbolProperty[];
switch?: ScadaSymbolProperty;
rowClass?: string;
}
export const toBehaviorGroups = (behaviors: ScadaSymbolBehavior[]): ScadaSymbolBehaviorGroup[] => { export const toBehaviorGroups = (behaviors: ScadaSymbolBehavior[]): ScadaSymbolBehaviorGroup[] => {
const result: ScadaSymbolBehaviorGroup[] = []; const result: ScadaSymbolBehaviorGroup[] = [];
for (const behavior of behaviors) { for (const behavior of behaviors) {
@ -54,24 +45,3 @@ export const toBehaviorGroups = (behaviors: ScadaSymbolBehavior[]): ScadaSymbolB
} }
return result; return result;
}; };
export const toPropertyRows = (properties: ScadaSymbolProperty[]): ScadaSymbolPropertyRow[] => {
const result: ScadaSymbolPropertyRow[] = [];
for (const property of properties) {
let propertyRow = result.find(r => r.label === property.name);
if (!propertyRow) {
propertyRow = {
label: property.name,
properties: [],
rowClass: property.rowClass
};
result.push(propertyRow);
}
if (property.type === ScadaSymbolPropertyType.switch) {
propertyRow.switch = property;
} else {
propertyRow.properties.push(property);
}
}
return result;
};

32
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -164,6 +164,22 @@ import {
import { import {
WidgetButtonToggleCustomStylePanelComponent WidgetButtonToggleCustomStylePanelComponent
} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component'; } from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component';
import {
DynamicFormPropertiesComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component';
import {
DynamicFormPropertyRowComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component';
import {
DynamicFormPropertyPanelComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component';
import { DynamicFormComponent } from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component';
import {
DynamicFormSelectItemsComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component';
import {
DynamicFormSelectItemRowComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -226,7 +242,13 @@ import {
DataKeyInputComponent, DataKeyInputComponent,
EntityAliasInputComponent, EntityAliasInputComponent,
AdvancedRangeComponent, AdvancedRangeComponent,
GradientComponent GradientComponent,
DynamicFormPropertiesComponent,
DynamicFormPropertyRowComponent,
DynamicFormPropertyPanelComponent,
DynamicFormSelectItemsComponent,
DynamicFormSelectItemRowComponent,
DynamicFormComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -293,7 +315,13 @@ import {
DataKeyInputComponent, DataKeyInputComponent,
EntityAliasInputComponent, EntityAliasInputComponent,
AdvancedRangeComponent, AdvancedRangeComponent,
GradientComponent GradientComponent,
DynamicFormPropertiesComponent,
DynamicFormPropertyRowComponent,
DynamicFormPropertyPanelComponent,
DynamicFormSelectItemsComponent,
DynamicFormSelectItemRowComponent,
DynamicFormComponent
], ],
providers: [ providers: [
ColorSettingsComponentService, ColorSettingsComponentService,

15
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -32,6 +32,9 @@
<div [class.!hidden]="selectedOption !== 'appearance'" class="mat-content"> <div [class.!hidden]="selectedOption !== 'appearance'" class="mat-content">
<ng-container *ngTemplateOutlet="appearance"></ng-container> <ng-container *ngTemplateOutlet="appearance"></ng-container>
</div> </div>
<div [class.!hidden]="selectedOption !== 'oldAppearance'" class="mat-content">
<ng-container *ngTemplateOutlet="oldAppearance"></ng-container>
</div>
<div [class.!hidden]="selectedOption !== 'card'" [formGroup]="widgetSettings" class="mat-content"> <div [class.!hidden]="selectedOption !== 'card'" [formGroup]="widgetSettings" class="mat-content">
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-title</div> <div class="tb-form-panel-title" translate>widget-config.card-title</div>
@ -310,5 +313,17 @@
</tb-widget-settings> </tb-widget-settings>
</div> </div>
</ng-template> </ng-template>
<ng-template #oldAppearance>
<div *ngIf="displayAdvancedAppearance" style="height: 100%;" [formGroup]="oldAdvancedSettings">
<tb-widget-settings
[aliasController]="aliasController"
[callbacks]="widgetConfigCallbacks"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfig]="modelValue"
formControlName="settings">
</tb-widget-settings>
</div>
</ng-template>
</div> </div>

48
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -32,7 +32,7 @@ import {
CellClickColumnInfo, CellClickColumnInfo,
DataKey, DataKey,
datasourcesHasAggregation, datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation, datasourcesHasOnlyComparisonAggregation, DynamicFormData,
GroupInfo, GroupInfo,
JsonSchema, JsonSchema,
JsonSettingsSchema, JsonSettingsSchema,
@ -190,6 +190,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
public widgetSettings: UntypedFormGroup; public widgetSettings: UntypedFormGroup;
public layoutSettings: UntypedFormGroup; public layoutSettings: UntypedFormGroup;
public advancedSettings: UntypedFormGroup; public advancedSettings: UntypedFormGroup;
public oldAdvancedSettings: UntypedFormGroup;
public actionsSettings: UntypedFormGroup; public actionsSettings: UntypedFormGroup;
private createBasicModeComponentTimeout: Timeout; private createBasicModeComponentTimeout: Timeout;
@ -203,6 +204,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
private widgetSettingsSubscription: Subscription; private widgetSettingsSubscription: Subscription;
private layoutSettingsSubscription: Subscription; private layoutSettingsSubscription: Subscription;
private advancedSettingsSubscription: Subscription; private advancedSettingsSubscription: Subscription;
private oldAdvancedSettingsSubscription: Subscription;
private actionsSettingsSubscription: Subscription; private actionsSettingsSubscription: Subscription;
private defaultConfigFormsType: widgetType; private defaultConfigFormsType: widgetType;
@ -221,6 +223,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
this.dataSettings = this.fb.group({}); this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({});
this.advancedSettings = this.fb.group({}); this.advancedSettings = this.fb.group({});
this.oldAdvancedSettings = this.fb.group({});
this.widgetSettings = this.fb.group({ this.widgetSettings = this.fb.group({
title: [null, []], title: [null, []],
titleFont: [null, []], titleFont: [null, []],
@ -296,6 +299,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
this.advancedSettingsSubscription.unsubscribe(); this.advancedSettingsSubscription.unsubscribe();
this.advancedSettingsSubscription = null; this.advancedSettingsSubscription = null;
} }
if (this.oldAdvancedSettingsSubscription) {
this.oldAdvancedSettingsSubscription.unsubscribe();
this.oldAdvancedSettingsSubscription = null;
}
if (this.actionsSettingsSubscription) { if (this.actionsSettingsSubscription) {
this.actionsSettingsSubscription.unsubscribe(); this.actionsSettingsSubscription.unsubscribe();
this.actionsSettingsSubscription = null; this.actionsSettingsSubscription = null;
@ -318,6 +325,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe( this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe(
() => this.updateAdvancedSettings() () => this.updateAdvancedSettings()
); );
this.oldAdvancedSettingsSubscription = this.oldAdvancedSettings.valueChanges.subscribe(
() => this.updateOldAdvancedSettings()
);
this.actionsSettingsSubscription = this.actionsSettings.valueChanges.subscribe( this.actionsSettingsSubscription = this.actionsSettings.valueChanges.subscribe(
() => this.updateActionSettings() () => this.updateActionSettings()
); );
@ -340,6 +350,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
value: 'appearance' value: 'appearance'
} }
); );
this.headerOptions.push(
{
name: 'Old appearance',
value: 'oldAppearance'
}
);
} }
this.headerOptions.push( this.headerOptions.push(
{ {
@ -393,6 +409,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
} }
this.advancedSettings.addControl('settings', this.advancedSettings.addControl('settings',
this.fb.control(null, [])); this.fb.control(null, []));
this.oldAdvancedSettings.addControl('settings',
this.fb.control(null, []));
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -563,7 +581,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
} }
} }
this.updateSchemaForm(config.settings); this.updateAdvancedForm(config.settings);
this.updateSchemaFormOld(config.settings);
if (layout) { if (layout) {
this.layoutSettings.patchValue( this.layoutSettings.patchValue(
@ -633,7 +652,19 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
} }
} }
private updateSchemaForm(settings?: any) { private updateAdvancedForm(settings?: any) {
const dynamicFormData: DynamicFormData = {};
dynamicFormData.model = settings || {};
if (this.modelValue.settingsForm?.length) {
dynamicFormData.settingsForm = this.modelValue.settingsForm;
} else {
dynamicFormData.settingsForm = [];
}
dynamicFormData.settingsDirective = this.modelValue.settingsDirective;
this.advancedSettings.patchValue({ settings: dynamicFormData }, {emitEvent: false});
}
private updateSchemaFormOld(settings?: any) {
const widgetSettingsFormData: JsonFormComponentData = {}; const widgetSettingsFormData: JsonFormComponentData = {};
if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) { if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) {
widgetSettingsFormData.schema = this.modelValue.settingsSchema.schema; widgetSettingsFormData.schema = this.modelValue.settingsSchema.schema;
@ -647,7 +678,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
widgetSettingsFormData.model = settings || {}; widgetSettingsFormData.model = settings || {};
} }
widgetSettingsFormData.settingsDirective = this.modelValue.settingsDirective; widgetSettingsFormData.settingsDirective = this.modelValue.settingsDirective;
this.advancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false}); this.oldAdvancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false});
} }
private updateDataSettings() { private updateDataSettings() {
@ -701,6 +732,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
} }
} }
private updateOldAdvancedSettings() {
if (this.modelValue) {
if (this.modelValue.config) {
this.modelValue.config.settings = this.advancedSettings.get('settings').value?.model;
}
this.propagateChange(this.modelValue);
}
}
private updateActionSettings() { private updateActionSettings() {
if (this.modelValue) { if (this.modelValue) {
if (this.modelValue.config) { if (this.modelValue.config) {

2
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -106,6 +106,7 @@ import { UserSettingsService } from '@core/http/user-settings.service';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { CompiledTbFunction } from '@shared/models/js-function.models'; import { CompiledTbFunction } from '@shared/models/js-function.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
export interface IWidgetAction { export interface IWidgetAction {
name: string; name: string;
@ -577,6 +578,7 @@ export interface WidgetConfigComponentData {
typeParameters: WidgetTypeParameters; typeParameters: WidgetTypeParameters;
actionSources: {[actionSourceId: string]: WidgetActionSource}; actionSources: {[actionSourceId: string]: WidgetActionSource};
isDataEnabled: boolean; isDataEnabled: boolean;
settingsForm: FormProperty[];
settingsSchema: JsonSettingsSchema; settingsSchema: JsonSettingsSchema;
dataKeySettingsSchema: JsonSettingsSchema; dataKeySettingsSchema: JsonSettingsSchema;
latestDataKeySettingsSchema: JsonSettingsSchema; latestDataKeySettingsSchema: JsonSettingsSchema;

14
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts

@ -35,15 +35,6 @@ import {
import { import {
ScadaSymbolBehaviorPanelComponent ScadaSymbolBehaviorPanelComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component'; } from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component';
import {
ScadaSymbolPropertiesComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-properties.component';
import {
ScadaSymbolPropertyRowComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component';
import {
ScadaSymbolPropertyPanelComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component';
import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module'; import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module';
import { import {
ScadaSymbolMetadataTagFunctionPanelComponent ScadaSymbolMetadataTagFunctionPanelComponent
@ -58,10 +49,7 @@ import {
ScadaSymbolMetadataTagFunctionPanelComponent, ScadaSymbolMetadataTagFunctionPanelComponent,
ScadaSymbolBehaviorsComponent, ScadaSymbolBehaviorsComponent,
ScadaSymbolBehaviorRowComponent, ScadaSymbolBehaviorRowComponent,
ScadaSymbolBehaviorPanelComponent, ScadaSymbolBehaviorPanelComponent
ScadaSymbolPropertiesComponent,
ScadaSymbolPropertyRowComponent,
ScadaSymbolPropertyPanelComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

4
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html

@ -106,9 +106,9 @@
</tb-scada-symbol-metadata-behaviors> </tb-scada-symbol-metadata-behaviors>
</div> </div>
<div [class.!hidden]="selectedOption !== 'properties'" class="mat-content overflow-hidden"> <div [class.!hidden]="selectedOption !== 'properties'" class="mat-content overflow-hidden">
<tb-scada-symbol-metadata-properties <tb-dynamic-form-properties
formControlName="properties"> formControlName="properties">
</tb-scada-symbol-metadata-properties> </tb-dynamic-form-properties>
</div> </div>
</div> </div>
</div> </div>

136
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts

@ -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;
}
}
}

43
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts

@ -27,14 +27,13 @@ import {
ScadaSymbolBehavior, ScadaSymbolBehavior,
ScadaSymbolBehaviorType, ScadaSymbolBehaviorType,
scadaSymbolContentData, scadaSymbolContentData,
ScadaSymbolMetadata, ScadaSymbolMetadata
ScadaSymbolProperty,
ScadaSymbolPropertyType
} from '@home/components/widget/lib/scada/scada-symbol.models'; } from '@home/components/widget/lib/scada/scada-symbol.models';
import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models'; import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { AceHighlightRule, AceHighlightRules } from '@shared/models/ace/ace.models'; import { AceHighlightRule, AceHighlightRules } from '@shared/models/ace/ace.models';
import { HelpLinks, ValueType } from '@shared/models/constants'; import { HelpLinks, ValueType } from '@shared/models/constants';
import { formPropertyCompletions } from '@shared/models/dynamic-form.models';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide; import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
import ITooltipsterHelper = JQueryTooltipster.ITooltipsterHelper; import ITooltipsterHelper = JQueryTooltipster.ITooltipsterHelper;
@ -1141,11 +1140,8 @@ export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags
meta: 'object', meta: 'object',
type: 'object', type: 'object',
description: 'An object holding all defined SCADA symbol properties.', description: 'An object holding all defined SCADA symbol properties.',
children: {} children: formPropertyCompletions(metadata.properties, customTranslate)
}; };
for (const property of metadata.properties) {
properties.children[property.id] = scadaSymbolPropertyCompletion(property, customTranslate);
}
const values: TbEditorCompletion = { const values: TbEditorCompletion = {
meta: 'object', meta: 'object',
type: 'object', type: 'object',
@ -1441,18 +1437,6 @@ export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags
}; };
}; };
const scadaSymbolPropertyCompletion = (property: ScadaSymbolProperty, customTranslate: CustomTranslatePipe): TbEditorCompletion => {
let description = customTranslate.transform(property.name, property.name);
if (property.subLabel) {
description += ` <small>${customTranslate.transform(property.subLabel, property.subLabel)}</small>`;
}
return {
meta: 'property',
description,
type: scadaSymbolPropertyCompletionType(property.type)
};
};
const scadaSymbolValueCompletion = (value: ScadaSymbolBehavior, customTranslate: CustomTranslatePipe): TbEditorCompletion => { const scadaSymbolValueCompletion = (value: ScadaSymbolBehavior, customTranslate: CustomTranslatePipe): TbEditorCompletion => {
const description = customTranslate.transform(value.name, value.name); const description = customTranslate.transform(value.name, value.name);
return { return {
@ -1462,27 +1446,6 @@ const scadaSymbolValueCompletion = (value: ScadaSymbolBehavior, customTranslate:
}; };
}; };
const scadaSymbolPropertyCompletionType = (type: ScadaSymbolPropertyType): string => {
switch (type) {
case ScadaSymbolPropertyType.text:
return 'string';
case ScadaSymbolPropertyType.number:
return 'number';
case ScadaSymbolPropertyType.switch:
return 'boolean';
case ScadaSymbolPropertyType.color:
return 'color string';
case ScadaSymbolPropertyType.color_settings:
return 'ColorProcessor';
case ScadaSymbolPropertyType.font:
return 'Font';
case ScadaSymbolPropertyType.units:
return 'units string';
case ScadaSymbolPropertyType.icon:
return 'icon string';
}
};
const scadaSymbolValueCompletionType = (type: ValueType): string => { const scadaSymbolValueCompletionType = (type: ValueType): string => {
switch (type) { switch (type) {
case ValueType.STRING: case ValueType.STRING:

9
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -188,6 +188,15 @@
</div> </div>
<div #topRightPanel class="tb-split tb-content"> <div #topRightPanel class="tb-split tb-content">
<mat-tab-group mat-stretch-tabs="false" dynamicHeight="true" style="width: 100%; height: 100%;"> <mat-tab-group mat-stretch-tabs="false" dynamicHeight="true" style="width: 100%; height: 100%;">
<mat-tab label="{{ 'widget.settings-form-properties' | translate }}">
<div class="tb-resize-container" style="background-color: #fff;">
<tb-dynamic-form-properties
[(ngModel)]="widget.settingsForm"
(ngModelChange)="settingsFormUpdated()"
noBorder fillHeight>
</tb-dynamic-form-properties>
</div>
</mat-tab>
<mat-tab label="{{ 'widget.settings-schema' | translate }}"> <mat-tab label="{{ 'widget.settings-schema' | translate }}">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonSettingsFullscreen"> <div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonSettingsFullscreen">
<div class="tb-editor-area-title-panel"> <div class="tb-editor-area-title-panel">

28
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts

@ -21,8 +21,10 @@ import {
EventEmitter, EventEmitter,
Inject, Inject,
OnDestroy, OnDestroy,
OnInit, Renderer2, OnInit,
ViewChild, ViewContainerRef, Renderer2,
ViewChild,
ViewContainerRef,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -65,12 +67,13 @@ import { Observable } from 'rxjs/internal/Observable';
import { catchError, map, tap } from 'rxjs/operators'; import { catchError, map, tap } from 'rxjs/operators';
import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models'; import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models';
import { HttpClient, HttpStatusCode } from '@angular/common/http'; import { HttpClient, HttpStatusCode } from '@angular/common/http';
import Timeout = NodeJS.Timeout;
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { loadModulesCompleter } from '@shared/models/js-function.models'; import { loadModulesCompleter } from '@shared/models/js-function.models';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component'; import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component';
import { MatIconButton } from '@angular/material/button'; import { MatIconButton } from '@angular/material/button';
import { formPropertyCompletions, jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import Timeout = NodeJS.Timeout;
// @dynamic // @dynamic
@Component({ @Component({
@ -197,6 +200,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private customTranslate: CustomTranslatePipe,
private http: HttpClient) { private http: HttpClient) {
super(store); super(store);
@ -223,6 +227,9 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
const config = JSON.parse(this.widget.defaultConfig); const config = JSON.parse(this.widget.defaultConfig);
this.widget.defaultConfig = JSON.stringify(config); this.widget.defaultConfig = JSON.stringify(config);
} }
if (!this.widget.settingsForm?.length) {
this.widget.settingsForm = jsonFormSchemaToFormProperties(this.widget.settingsSchema);
}
this.origWidget = deepClone(this.widget); this.origWidget = deepClone(this.widget);
if (!this.widgetTypeDetails) { if (!this.widgetTypeDetails) {
this.isDirty = true; this.isDirty = true;
@ -405,6 +412,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.jsEditor.on('change', () => { this.jsEditor.on('change', () => {
this.cleanupJsErrors(); this.cleanupJsErrors();
}); });
if (!(this.jsEditor as any).completer) {
this.jsEditor.execCommand("startAutocomplete");
(this.jsEditor as any).completer.detach();
}
(this.jsEditor as any).completer.popup.container.style.width = '500px';
this.initialCompleters = this.jsEditor.completers || []; this.initialCompleters = this.jsEditor.completers || [];
}) })
)); ));
@ -873,6 +885,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.isDirty = true; this.isDirty = true;
} }
settingsFormUpdated() {
this.isDirty = true;
this.updateControllerScriptCompleters();
}
editControllerScriptModules($event: Event, button: MatIconButton) { editControllerScriptModules($event: Event, button: MatIconButton) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -951,7 +968,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
const modulesCompleterObservable = loadModulesCompleter(this.http, this.controllerScriptModules); const modulesCompleterObservable = loadModulesCompleter(this.http, this.controllerScriptModules);
modulesCompleterObservable.subscribe((modulesCompleter) => { modulesCompleterObservable.subscribe((modulesCompleter) => {
const completers: Ace.Completer[] = []; const completers: Ace.Completer[] = [];
completers.push(widgetEditorCompleter); const formPropertiesCompletions = formPropertyCompletions(this.widget.settingsForm || [], this.customTranslate);
completers.push(widgetEditorCompleter(formPropertiesCompletions));
if (modulesCompleter) { if (modulesCompleter) {
completers.push(modulesCompleter); completers.push(modulesCompleter);
} }

145
ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts

@ -15,77 +15,84 @@
/// ///
import { TbEditorCompleter, TbEditorCompletions } from '@shared/models/ace/completion.models'; import { TbEditorCompleter, TbEditorCompletions } from '@shared/models/ace/completion.models';
import { widgetContextCompletions } from '@shared/models/ace/widget-completion.models'; import {
widgetContextCompletions,
widgetContextCompletionsWithSettings
} from '@shared/models/ace/widget-completion.models';
import { serviceCompletions } from '@shared/models/ace/service-completion.models'; import { serviceCompletions } from '@shared/models/ace/service-completion.models';
const widgetEditorCompletions: TbEditorCompletions = { const widgetEditorCompletions = (settingsCompletions?: TbEditorCompletions): TbEditorCompletions => {
... {self: { return {
description: 'Built-in variable <b>self</b> that is a reference to the widget instance', ... {self: {
type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L350" target="_blank">WidgetTypeInstance</a>', description: 'Built-in variable <b>self</b> that is a reference to the widget instance',
meta: 'object', type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L350" target="_blank">WidgetTypeInstance</a>',
children: { meta: 'object',
...{ children: {
onInit: { ...{
description: 'The first function which is called when widget is ready for initialization.<br>Should be used to prepare widget DOM, process widget settings and initial subscription information.', onInit: {
meta: 'function' description: 'The first function which is called when widget is ready for initialization.<br>Should be used to prepare widget DOM, process widget settings and initial subscription information.',
}, meta: 'function'
onDataUpdated: { },
description: 'Called when the new data is available from the widget subscription.<br>Latest data can be accessed from ' + onDataUpdated: {
'the <code>defaultSubscription</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.', description: 'Called when the new data is available from the widget subscription.<br>Latest data can be accessed from ' +
meta: 'function' 'the <code>defaultSubscription</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
}, meta: 'function'
onResize: { },
description: 'Called when widget container is resized. Latest <code>width</code> and <code>height</code> can be obtained from <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.', onResize: {
meta: 'function' description: 'Called when widget container is resized. Latest <code>width</code> and <code>height</code> can be obtained from <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
}, meta: 'function'
onEditModeChanged: { },
description: 'Called when dashboard editing mode is changed. Latest mode is handled by <code>isEdit</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.', onEditModeChanged: {
meta: 'function' description: 'Called when dashboard editing mode is changed. Latest mode is handled by <code>isEdit</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
}, meta: 'function'
onMobileModeChanged: { },
description: 'Called when dashboard view width crosses mobile breakpoint. Latest state is handled by <code>isMobile</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.', onMobileModeChanged: {
meta: 'function' description: 'Called when dashboard view width crosses mobile breakpoint. Latest state is handled by <code>isMobile</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
}, meta: 'function'
onDestroy: { },
description: 'Called when widget element is destroyed. Should be used to cleanup all resources if necessary.', onDestroy: {
meta: 'function' description: 'Called when widget element is destroyed. Should be used to cleanup all resources if necessary.',
}, meta: 'function'
getSettingsSchema: { },
description: 'Optional function returning widget settings schema json as alternative to <b>Settings tab</b> of <a href="https://thingsboard.io/docs/user-guide/contribution/widgets-development/#settings-schema-section" target="_blank">Settings schema section</a>.', getSettingsSchema: {
meta: 'function', description: 'Optional function returning widget settings schema json as alternative to <b>Settings tab</b> of <a href="https://thingsboard.io/docs/user-guide/contribution/widgets-development/#settings-schema-section" target="_blank">Settings schema section</a>.',
return: { meta: 'function',
description: 'An widget settings schema json', return: {
type: 'object' description: 'An widget settings schema json',
} type: 'object'
}, }
getDataKeySettingsSchema: { },
description: 'Optional function returning particular data key settings schema json as alternative to <b>Data key settings schema</b> of <a href="https://thingsboard.io/docs/user-guide/contribution/widgets-development/#settings-schema-section" target="_blank">Settings schema section</a>.', getDataKeySettingsSchema: {
meta: 'function', description: 'Optional function returning particular data key settings schema json as alternative to <b>Data key settings schema</b> of <a href="https://thingsboard.io/docs/user-guide/contribution/widgets-development/#settings-schema-section" target="_blank">Settings schema section</a>.',
return: { meta: 'function',
description: 'A particular data key settings schema json', return: {
type: 'object' description: 'A particular data key settings schema json',
} type: 'object'
}, }
typeParameters: { },
description: 'Returns object describing widget datasource parameters.', typeParameters: {
meta: 'function', description: 'Returns object describing widget datasource parameters.',
return: { meta: 'function',
description: 'An object describing widget datasource parameters.', return: {
type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L146" target="_blank">WidgetTypeParameters</a>' description: 'An object describing widget datasource parameters.',
} type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L146" target="_blank">WidgetTypeParameters</a>'
}, }
actionSources: { },
description: 'Returns map describing available widget action sources used to define user actions.', actionSources: {
meta: 'function', description: 'Returns map describing available widget action sources used to define user actions.',
return: { meta: 'function',
description: 'A map of action sources by action source id.', return: {
type: '{[actionSourceId: string]: <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L118" target="_blank">WidgetActionSource</a>}' description: 'A map of action sources by action source id.',
} type: '{[actionSourceId: string]: <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L118" target="_blank">WidgetActionSource</a>}'
}
}
},
...widgetContextCompletionsWithSettings(settingsCompletions)
} }
}, }}
...widgetContextCompletions }
}
}}
}; };
export const widgetEditorCompleter = new TbEditorCompleter(widgetEditorCompletions); export const widgetEditorCompleter = (settingsCompletions?: TbEditorCompletions): TbEditorCompleter => {
return new TbEditorCompleter(widgetEditorCompletions(settingsCompletions));
}

2
ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts

@ -29,6 +29,7 @@ import { WidgetTypeTabsComponent } from '@home/pages/widget/widget-type-tabs.com
import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component'; import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component';
import { WidgetTypeAutocompleteComponent } from '@home/pages/widget/widget-type-autocomplete.component'; import { WidgetTypeAutocompleteComponent } from '@home/pages/widget/widget-type-autocomplete.component';
import { WidgetsBundleDialogComponent } from '@home/pages/widget/widgets-bundle-dialog.component'; import { WidgetsBundleDialogComponent } from '@home/pages/widget/widgets-bundle-dialog.component';
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -47,6 +48,7 @@ import { WidgetsBundleDialogComponent } from '@home/pages/widget/widgets-bundle-
CommonModule, CommonModule,
SharedModule, SharedModule,
HomeComponentsModule, HomeComponentsModule,
WidgetConfigComponentsModule,
WidgetLibraryRoutingModule WidgetLibraryRoutingModule
] ]
}) })

15
ui-ngx/src/app/shared/components/value-input.component.html

@ -68,11 +68,16 @@
warning warning
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
<mat-chip-listbox *ngIf="valueType === valueTypeEnum.BOOLEAN" class="tb-boolean-input center-stretch flex-1" <ng-container *ngIf="valueType === valueTypeEnum.BOOLEAN">
[disabled]="disabled" name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()"> <mat-chip-listbox *ngIf="!shortBooleanField" class="tb-boolean-input center-stretch flex-1"
<mat-chip-option [selectable]="!modelValue" [value]="true">{{ trueLabel }}</mat-chip-option> [disabled]="disabled" name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()">
<mat-chip-option [selectable]="modelValue" [value]="false">{{ falseLabel }}</mat-chip-option> <mat-chip-option [selectable]="!modelValue" [value]="true">{{ trueLabel }}</mat-chip-option>
</mat-chip-listbox> <mat-chip-option [selectable]="modelValue" [value]="false">{{ falseLabel }}</mat-chip-option>
</mat-chip-listbox>
<mat-checkbox *ngIf="shortBooleanField" name="value" #value="ngModel" [disabled]="disabled" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()">
{{ modelValue ? trueLabel : falseLabel }}
</mat-checkbox>
</ng-container>
<div *ngIf="valueType === valueTypeEnum.JSON" class="flex flex-1 flex-row items-center justify-start gap-2"> <div *ngIf="valueType === valueTypeEnum.JSON" class="flex flex-1 flex-row items-center justify-start gap-2">
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1"> <mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1">
<input [disabled]="disabled" matInput tb-json-to-string required name="value" #value="ngModel" <input [disabled]="disabled" matInput tb-json-to-string required name="value" #value="ngModel"

11
ui-ngx/src/app/shared/components/value-input.component.ts

@ -36,6 +36,7 @@ import {
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
type Layout = 'column' | 'row'; type Layout = 'column' | 'row';
@ -67,12 +68,19 @@ export class ValueInputComponent implements OnInit, OnDestroy, OnChanges, Contro
@Input() @Input()
valueType: ValueType; valueType: ValueType;
@Input()
allowedValueTypes: ValueType[];
@Input() @Input()
trueLabel: string; trueLabel: string;
@Input() @Input()
falseLabel: string; falseLabel: string;
@Input()
@coerceBoolean()
shortBooleanField = false;
@Input() @Input()
layout: ValueInputLayout | Layout = 'row'; layout: ValueInputLayout | Layout = 'row';
@ -110,6 +118,9 @@ export class ValueInputComponent implements OnInit, OnDestroy, OnChanges, Contro
if (!this.falseLabel) { if (!this.falseLabel) {
this.falseLabel = this.translate.instant('value.false'); this.falseLabel = this.translate.instant('value.false');
} }
if (this.allowedValueTypes?.length) {
this.valueTypeKeys = this.allowedValueTypes;
}
this._subscription = new Subscription(); this._subscription = new Subscription();
this.showValueType = !this.valueType; this.showValueType = !this.valueType;
this.computedLayout = this._computeLayout(); this.computedLayout = this._computeLayout();

1184
ui-ngx/src/app/shared/models/ace/widget-completion.models.ts

File diff suppressed because it is too large

385
ui-ngx/src/app/shared/models/dynamic-form.models.ts

@ -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&lt;${typesString}&gt;`;
} 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;
}

8
ui-ngx/src/app/shared/models/widget.models.ts

@ -45,6 +45,7 @@ import { HasTenantId, HasVersion } from '@shared/models/entity.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { TbFunction } from '@shared/models/js-function.models'; import { TbFunction } from '@shared/models/js-function.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
export enum widgetType { export enum widgetType {
timeseries = 'timeseries', timeseries = 'timeseries',
@ -153,6 +154,7 @@ export interface WidgetTypeDescriptor {
templateHtml: string; templateHtml: string;
templateCss: string; templateCss: string;
controllerScript: TbFunction; controllerScript: TbFunction;
settingsForm?: FormProperty[];
settingsSchema?: string | any; settingsSchema?: string | any;
dataKeySettingsSchema?: string | any; dataKeySettingsSchema?: string | any;
latestDataKeySettingsSchema?: string | any; latestDataKeySettingsSchema?: string | any;
@ -830,6 +832,12 @@ export interface JsonSettingsSchema {
groupInfoes?: GroupInfo[]; groupInfoes?: GroupInfo[];
} }
export interface DynamicFormData {
settingsForm?: FormProperty[];
model?: any;
settingsDirective?: string;
}
export interface WidgetPosition { export interface WidgetPosition {
row: number; row: number;
column: number; column: number;

80
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1617,6 +1617,53 @@
"lwm2m-command": "Use the following documentation to connect the device through the LWM2M." "lwm2m-command": "Use the following documentation to connect the device through the LWM2M."
} }
}, },
"dynamic-form": {
"property": {
"properties": "Properties",
"property": "Property",
"id": "Id",
"name": "Name",
"type": "Type",
"type-text": "Text",
"type-number": "Number",
"type-switch": "Switch",
"type-select": "Select",
"type-color": "Color",
"type-color-settings": "Color settings",
"type-font": "Font",
"type-units": "Units",
"type-icon": "Icon",
"type-fieldset": "Fieldset",
"group-title": "Group title",
"no-properties": "No properties configured",
"add-property": "Add property",
"property-settings": "Property settings",
"remove-property": "Remove property",
"default-value": "Default value",
"value-required": "Value required",
"number-settings": "Number settings",
"min": "Min",
"max": "Max",
"step": "Step",
"advanced-ui-settings": "Advanced UI settings",
"disable-on-property": "Disable on property",
"display-condition-function": "Display condition function",
"sub-label": "Sub label",
"vertical-divider-after": "Vertical divider after",
"input-field-suffix": "Input field suffix",
"property-row-classes": "Property row classes",
"property-field-classes": "Property field classes",
"not-unique-property-ids-error": "Property Ids must be unique!",
"enable-multiple-select": "Enable multiple select",
"select-options": "Select options",
"not-unique-select-option-value-error": "Select option values must be unique!",
"value": "Value",
"label": "Label",
"add-option": "Add option",
"no-options": "No options configured",
"remove-option": "Remove option"
}
},
"asset-profile": { "asset-profile": {
"asset-profile": "Asset profile", "asset-profile": "Asset profile",
"asset-profiles": "Asset profiles", "asset-profiles": "Asset profiles",
@ -3099,38 +3146,6 @@
"not-unique-behavior-ids-error": "Behavior Ids must be unique!", "not-unique-behavior-ids-error": "Behavior Ids must be unique!",
"default-settings": "Default settings" "default-settings": "Default settings"
}, },
"property": {
"property": "Property",
"id": "Id",
"name": "Name",
"type": "Type",
"type-text": "Text",
"type-number": "Number",
"type-switch": "Switch",
"type-color": "Color",
"type-color-settings": "Color settings",
"type-font": "Font",
"type-units": "Units",
"type-icon": "Icon",
"no-properties": "No properties configured",
"add-property": "Add property",
"property-settings": "Property settings",
"remove-property": "Remove property",
"default-value": "Default value",
"value-required": "Value required",
"number-settings": "Number settings",
"min": "Min",
"max": "Max",
"step": "Step",
"advanced-ui-settings": "Advanced UI settings",
"disable-on-property": "Disable on property",
"sub-label": "Sub label",
"vertical-divider-after": "Vertical divider after",
"input-field-suffix": "Input field suffix",
"property-row-classes": "Property row classes",
"property-field-classes": "Property field classes",
"not-unique-property-ids-error": "Property Ids must be unique!"
},
"symbol": { "symbol": {
"symbol": "SCADA symbol", "symbol": "SCADA symbol",
"fluid-presence": "Fluid presence", "fluid-presence": "Fluid presence",
@ -5429,6 +5444,7 @@
"html": "HTML", "html": "HTML",
"tidy": "Tidy", "tidy": "Tidy",
"css": "CSS", "css": "CSS",
"settings-form-properties": "Settings form properties",
"settings-schema": "Settings schema", "settings-schema": "Settings schema",
"datakey-settings-schema": "Data key settings schema", "datakey-settings-schema": "Data key settings schema",
"latest-datakey-settings-schema": "Latest data key settings schema", "latest-datakey-settings-schema": "Latest data key settings schema",

Loading…
Cancel
Save