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 { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models';
export interface AddWidgetDialogData {
dashboard: Dashboard;
@ -108,6 +109,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
let settingsForm = jsonFormSchemaToFormProperties(rawSettingsSchema);
let settingsSchema;
if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {};
@ -142,6 +144,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
typeParameters,
actionSources,
isDataEnabled,
settingsForm,
settingsSchema,
dataKeySettingsSchema,
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 { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models';
@Component({
selector: 'tb-edit-widget',
@ -146,6 +147,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
const settingsForm = jsonFormSchemaToFormProperties(rawSettingsSchema);
let settingsSchema;
if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {};
@ -173,6 +175,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
typeParameters,
actionSources,
isDataEnabled,
settingsForm,
settingsSchema,
dataKeySettingsSchema,
latestDataKeySettingsSchema,

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

@ -18,7 +18,11 @@
<div [formGroup]="widgetSettingsFormGroup">
<ng-container #definedSettingsContent></ng-container>
<div class="tb-settings-directive-error" *ngIf="definedDirectiveError">{{definedDirectiveError}}</div>
<tb-json-form *ngIf="useJsonForm()" #jsonFormComponent
<tb-json-form *ngIf="useJsonForm()"
formControlName="settings">
</tb-json-form>
<tb-dynamic-form *ngIf="useDynamicForm()"
[properties]="settingsForm"
formControlName="settings">
</tb-dynamic-form>
</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 { JsonFormComponent } from '@shared/components/json-form/json-form.component';
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 { Dashboard } from '@shared/models/dashboard.models';
import { WidgetService } from '@core/http/widget.service';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
@Component({
selector: 'tb-widget-settings',
@ -67,8 +68,6 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
@ViewChild('definedSettingsContent', {read: ViewContainerRef, static: true}) definedSettingsContainer: ViewContainerRef;
@ViewChild('jsonFormComponent') jsonFormComponent: JsonFormComponent;
@Input()
disabled: boolean;
@ -91,6 +90,8 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
definedDirectiveError: string;
settingsForm?: FormProperty[];
widgetSettingsFormGroup: UntypedFormGroup;
changeSubscription: Subscription;
@ -98,7 +99,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
private definedSettingsComponentRef: ComponentRef<IWidgetSettingsComponent>;
private definedSettingsComponent: IWidgetSettingsComponent;
private widgetSettingsFormData: JsonFormComponentData;
private widgetSettingsFormData: JsonFormComponentData | DynamicFormData;
private propagateChange = (_v: any) => { };
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;
if ('settingsForm' in this.widgetSettingsFormData) {
this.settingsForm = this.widgetSettingsFormData.settingsForm;
} else {
this.settingsForm = null;
}
if (this.changeSubscription) {
this.changeSubscription.unsubscribe();
this.changeSubscription = null;
@ -181,10 +187,12 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
this.updateModel(settings);
});
} 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(
(widgetSettingsFormData: JsonFormComponentData) => {
this.updateModel(widgetSettingsFormData.model);
(data: JsonFormComponentData | WidgetSettings) => {
const settings = this.useJsonForm() ? data.model : data;
this.updateModel(settings);
}
);
}
@ -196,7 +204,11 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
}
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) {
@ -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) {
return {
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 { MatIconRegistry } from '@angular/material/icon';
import { RafService } from '@core/services/raf.service';
import { defaultFormPropertyValue, FormProperty, FormPropertyType } from '@shared/models/dynamic-form.models';
export interface ScadaSymbolApi {
generateElementId: () => string;
@ -151,60 +152,6 @@ export interface ScadaSymbolBehaviorAction extends ScadaSymbolBehaviorBase {
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 {
title: string;
description?: string;
@ -215,7 +162,7 @@ export interface ScadaSymbolMetadata {
stateRender?: ScadaSymbolStateRenderFunction;
tags: ScadaSymbolTag[];
behavior: ScadaSymbolBehavior[];
properties: ScadaSymbolProperty[];
properties: FormProperty[];
}
export const emptyMetadata = (width?: number, height?: number): ScadaSymbolMetadata => ({
@ -507,7 +454,7 @@ export const defaultScadaSymbolObjectSettings = (metadata: ScadaSymbolMetadata):
}
}
for (const property of metadata.properties) {
settings.properties[property.id] = property.default;
settings.properties[property.id] = defaultFormPropertyValue(property);
}
return settings;
};
@ -1020,33 +967,73 @@ export class ScadaSymbolObject {
return Array.isArray(element) ? element : [element];
}
private getProperty(id: string): ScadaSymbolProperty {
return this.metadata.properties.find(p => p.id === id);
private getProperty(...ids: string[]): FormProperty {
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 {
const property = this.getProperty(id);
private getPropertyValue(...ids: string[]): any {
const property = this.getProperty(...ids);
if (property) {
const value = this.settings.properties[id];
if (isDefinedAndNotNull(value)) {
if (property.type === ScadaSymbolPropertyType.color_settings) {
return ColorProcessor.fromSettings(value);
} else if (property.type === ScadaSymbolPropertyType.text) {
const result = this.ctx.utilsService.customTranslation(value, value);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
return createLabelFromSubscriptionEntityInfo(entityInfo, result);
if (property.type === FormPropertyType.fieldset) {
const propertyValue: {[id: string]: any} = {};
for (const childProperty of property.properties) {
propertyValue[childProperty.id] = this.getPropertyValue(...ids, childProperty.id);
}
return value;
return propertyValue;
} else {
switch (property.type) {
case ScadaSymbolPropertyType.text:
return '';
case ScadaSymbolPropertyType.number:
return 0;
case ScadaSymbolPropertyType.color:
return '#000';
case ScadaSymbolPropertyType.color_settings:
return ColorProcessor.fromSettings(constantColor('#000'));
const value = this.getSettingsValue(...ids);
if (isDefinedAndNotNull(value)) {
if (property.type === FormPropertyType.color_settings) {
return ColorProcessor.fromSettings(value);
} else if (property.type === FormPropertyType.text) {
const result = this.ctx.utilsService.customTranslation(value, value);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
return createLabelFromSubscriptionEntityInfo(entityInfo, result);
}
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 {

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

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
* limitations under the License.
*/
.tb-scada-symbol-properties {
.tb-dynamic-form-properties {
flex: 1;
margin: 12px;
&:not(.no-margin) {
margin: 12px;
}
.tb-form-table-header-cell {
&.tb-id-header {
flex: 1 1 40%;
@ -40,7 +42,7 @@
}
.tb-form-table-body {
overflow: auto;
tb-scada-symbol-metadata-property-row {
tb-dynamic-form-property-row {
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 {
Component,
Component, DestroyRef,
forwardRef,
HostBinding,
Input,
@ -35,43 +35,61 @@ import {
UntypedFormGroup,
Validator
} from '@angular/forms';
import { ScadaSymbolProperty, ScadaSymbolPropertyType } from '@home/components/widget/lib/scada/scada-symbol.models';
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 { 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({
selector: 'tb-scada-symbol-metadata-properties',
templateUrl: './scada-symbol-properties.component.html',
styleUrls: ['./scada-symbol-properties.component.scss'],
selector: 'tb-dynamic-form-properties',
templateUrl: './dynamic-form-properties.component.html',
styleUrls: ['./dynamic-form-properties.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaSymbolPropertiesComponent),
useExisting: forwardRef(() => DynamicFormPropertiesComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ScadaSymbolPropertiesComponent),
useExisting: forwardRef(() => DynamicFormPropertiesComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnInit, Validator {
export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnInit, Validator {
@HostBinding('style.display') styleDisplay = 'flex';
@HostBinding('style.overflow') styleOverflow = 'hidden';
@HostBinding('style.height')
get containerHeight(): string {
return this.fillHeight ? '100%': 'auto';
}
@ViewChildren(ScadaSymbolPropertyRowComponent)
propertyRows: QueryList<ScadaSymbolPropertyRowComponent>;
@ViewChildren(DynamicFormPropertyRowComponent)
propertyRows: QueryList<DynamicFormPropertyRowComponent>;
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
noBorder = false;
@Input()
@coerceBoolean()
noMargin = false;
@Input()
@coerceBoolean()
fillHeight = false;
booleanPropertyIds: string[] = [];
propertiesFormGroup: UntypedFormGroup;
@ -85,6 +103,7 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef,
private translate: TranslateService) {
}
@ -92,13 +111,15 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
this.propertiesFormGroup = this.fb.group({
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) {
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) => {
if (p.disableOnProperty && !this.booleanPropertyIds.includes(p.disableOnProperty)) {
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 || [];
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) {
@ -141,7 +162,7 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
for (const control of notUniqueControls) {
control.updateValueAndValidity({onlySelf: false, emitEvent: false});
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;
@ -185,10 +206,10 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
}
addProperty() {
const property: ScadaSymbolProperty = {
const property: FormProperty = {
id: '',
name: '',
type: ScadaSymbolPropertyType.text,
type: FormPropertyType.text,
default: ''
};
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> = [];
if (properties) {
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.
-->
<div class="tb-scada-symbol-property-settings-panel" [formGroup]="propertyFormGroup">
<div class="tb-scada-symbol-property-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-scada-symbol-property-settings-panel-content">
<div class="tb-dynamic-form-property-settings-panel" [formGroup]="propertyFormGroup">
<div class="tb-dynamic-form-property-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-dynamic-form-property-settings-panel-content">
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.id</div>
<div class="fixed-title-width" translate>dynamic-form.property.id</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.name</div>
<div class="fixed-title-width" translate>dynamic-form.property.name</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>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-select formControlName="type">
<mat-option *ngFor="let type of scadaSymbolPropertyTypes" [value]="type">
{{ scadaSymbolPropertyTypeTranslations.get(type) | translate }}
<mat-option *ngFor="let type of formPropertyTypes" [value]="type">
{{ formPropertyTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.default-value</div>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.text"
<div *ngIf="propertyFormGroup.get('type').value === FormPropertyType.number" class="tb-form-row space-between">
<div translate>dynamic-form.property.number-settings</div>
<div class="flex flex-1 flex-row items-center justify-end gap-2">
<div class="tb-small-label" translate>dynamic-form.property.min</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="min"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>dynamic-form.property.max</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="max"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>dynamic-form.property.step</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="step" min="0"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div *ngIf="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"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="default" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.color"
<tb-color-input *ngIf="propertyFormGroup.get('type').value === FormPropertyType.color"
asBoxInput
colorClearButton
formControlName="default">
</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"
settingsKey="{{ propertyFormGroup.get('id').value }}">
</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"
class="number"
subscriptSizing="dynamic">
@ -66,78 +128,69 @@
[step]="propertyFormGroup.get('step').value"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.font"
<tb-font-settings *ngIf="propertyFormGroup.get('type').value === FormPropertyType.font"
formControlName="default"
clearButton
disabledLineHeight>
</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>
<tb-value-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.switch"
<tb-value-input *ngIf="propertyFormGroup.get('type').value === FormPropertyType.switch"
[valueType]="ValueType.BOOLEAN"
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
formControlName="default">
</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 *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.number" class="tb-form-row space-between">
<div translate>scada.property.number-settings</div>
<div class="flex flex-1 flex-row items-center justify-end gap-2">
<div class="tb-small-label" translate>scada.property.min</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="min"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>scada.property.max</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="max"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>scada.property.step</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="step" min="0"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between">
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row space-between">
<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>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
{{ 'scada.property.advanced-ui-settings' | translate }}
{{ 'dynamic-form.property.advanced-ui-settings' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.sub-label</div>
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.fieldset" class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.sub-label</div>
<mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="subLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<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">
{{ 'scada.property.vertical-divider-after' | translate }}
{{ 'dynamic-form.property.vertical-divider-after' | translate }}
</mat-slide-toggle>
</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">
<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"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="fieldSuffix" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.disable-on-property</div>
<div class="fixed-title-width" translate>dynamic-form.property.disable-on-property</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="disableOnProperty" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="null"></mat-option>
@ -148,20 +201,28 @@
</mat-form-field>
</div>
<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-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 }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="propertyFormGroup.get('type').value !== ScadaSymbolPropertyType.switch" class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.property-field-classes</div>
<div *ngIf="![FormPropertyType.fieldset, FormPropertyType.switch].includes(propertyFormGroup.get('type').value)" class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.property-field-classes</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="fieldClass" multiple placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let clazz of scadaSymbolPropertyFieldClasses" [value]="clazz">
<mat-option *ngFor="let clazz of formPropertyFieldClasses" [value]="clazz">
{{ clazz }}
</mat-option>
</mat-select>
@ -171,7 +232,7 @@
</mat-expansion-panel>
</div>
</div>
<div class="tb-scada-symbol-property-settings-panel-buttons">
<div class="tb-dynamic-form-property-settings-panel-buttons">
<button mat-button
color="primary"
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
* limitations under the License.
*/
@import '../../../../../../scss/constants';
@import '../../../../../../../../../scss/constants';
.tb-scada-symbol-property-settings-panel {
.tb-dynamic-form-property-settings-panel {
width: 540px;
display: flex;
flex-direction: column;
@ -23,7 +23,7 @@
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-scada-symbol-property-settings-panel-content {
.tb-dynamic-form-property-settings-panel-content {
display: flex;
flex-direction: column;
gap: 16px;
@ -31,14 +31,14 @@
margin: -10px;
padding: 10px;
}
.tb-scada-symbol-property-settings-title {
.tb-dynamic-form-property-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-scada-symbol-property-settings-panel-buttons {
.tb-dynamic-form-property-settings-panel-buttons {
height: 40px;
display: flex;
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.
-->
<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">
<input #idInput required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
@ -24,8 +24,8 @@
</mat-form-field>
<mat-form-field class="tb-inline-field tb-type-field fixed-height" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of scadaSymbolPropertyTypes" [value]="type">
{{ scadaSymbolPropertyTypeTranslations.get(type) | translate }}
<mat-option *ngFor="let type of formPropertyTypes" [value]="type">
{{ formPropertyTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
@ -34,7 +34,7 @@
mat-icon-button
#editButton
(click)="editProperty($event, editButton)"
matTooltip="{{ 'scada.property.property-settings' | translate }}"
matTooltip="{{ 'dynamic-form.property.property-settings' | translate }}"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
@ -42,7 +42,7 @@
type="button"
mat-icon-button
(click)="propertyRemoved.emit()"
matTooltip="{{ 'scada.property.remove-property' | translate }}"
matTooltip="{{ 'dynamic-form.property.remove-property' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</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
* limitations under the License.
*/
.tb-scada-symbol-metadata-property-row {
.tb-dynamic-form-property-row {
.tb-id-field {
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 {
ChangeDetectorRef,
Component,
Component, DestroyRef,
ElementRef,
EventEmitter,
forwardRef,
@ -39,38 +39,39 @@ import {
ValidatorFn,
Validators
} from '@angular/forms';
import {
ScadaSymbolProperty,
ScadaSymbolPropertyType,
scadaSymbolPropertyTypes,
scadaSymbolPropertyTypeTranslations
} from '@home/components/widget/lib/scada/scada-symbol.models';
import { deepClone } from '@core/utils';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { constantColor, Font } from '@shared/models/widget-settings.models';
import {
ScadaSymbolPropertyPanelComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component';
FormProperty,
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 {
ScadaSymbolPropertiesComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-properties.component';
DynamicFormPropertyPanelComponent
} 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) {
case ScadaSymbolPropertyType.text:
case FormPropertyType.text:
return '';
case ScadaSymbolPropertyType.number:
case FormPropertyType.number:
return 0;
case ScadaSymbolPropertyType.switch:
case FormPropertyType.switch:
return false;
case ScadaSymbolPropertyType.color:
case FormPropertyType.color:
return '#000';
case ScadaSymbolPropertyType.color_settings:
case FormPropertyType.color_settings:
return constantColor('#000');
case ScadaSymbolPropertyType.font:
case FormPropertyType.font:
return {
size: 12,
sizeUnit: 'px',
@ -79,32 +80,35 @@ export const defaultPropertyValue = (type: ScadaSymbolPropertyType): any => {
style: 'normal',
lineHeight: '1'
} as Font;
case ScadaSymbolPropertyType.units:
case FormPropertyType.units:
return '';
case ScadaSymbolPropertyType.icon:
case FormPropertyType.icon:
return 'star';
case FormPropertyType.fieldset:
case FormPropertyType.select:
return null;
}
};
@Component({
selector: 'tb-scada-symbol-metadata-property-row',
templateUrl: './scada-symbol-property-row.component.html',
styleUrls: ['./scada-symbol-property-row.component.scss'],
selector: 'tb-dynamic-form-property-row',
templateUrl: './dynamic-form-property-row.component.html',
styleUrls: ['./dynamic-form-property-row.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent),
useExisting: forwardRef(() => DynamicFormPropertyRowComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent),
useExisting: forwardRef(() => DynamicFormPropertyRowComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, OnInit, Validator {
export class DynamicFormPropertyRowComponent implements ControlValueAccessor, OnInit, Validator {
@ViewChild('idInput')
idInput: ElementRef<HTMLInputElement>;
@ -112,8 +116,8 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
@ViewChild('editButton')
editButton: MatButton;
scadaSymbolPropertyTypes = scadaSymbolPropertyTypes;
scadaSymbolPropertyTypeTranslations = scadaSymbolPropertyTypeTranslations;
formPropertyTypes = formPropertyTypes;
formPropertyTypeTranslations = formPropertyTypeTranslations;
@Input()
disabled: boolean;
@ -129,16 +133,17 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
propertyRowFormGroup: UntypedFormGroup;
modelValue: ScadaSymbolProperty;
modelValue: FormProperty;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef,
private cd: ChangeDetectorRef,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private propertiesComponent: ScadaSymbolPropertiesComponent) {
private propertiesComponent: DynamicFormPropertiesComponent) {
}
ngOnInit() {
@ -147,10 +152,14 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
name: [null, [Validators.required]],
type: [null, [Validators.required]]
});
this.propertyRowFormGroup.valueChanges.subscribe(
this.propertyRowFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => 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);
});
}
@ -171,7 +180,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
}
}
writeValue(value: ScadaSymbolProperty): void {
writeValue(value: FormProperty): void {
this.modelValue = value;
this.propertyRowFormGroup.patchValue(
{
@ -197,14 +206,14 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
booleanPropertyIds: this.booleanPropertyIds,
property: deepClone(this.modelValue)
};
const scadaSymbolPropertyPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ScadaSymbolPropertyPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
const dynamicFormPropertyPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, DynamicFormPropertyPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
scadaSymbolPropertyPanelPopover.tbComponentRef.instance.popover = scadaSymbolPropertyPanelPopover;
scadaSymbolPropertyPanelPopover.tbComponentRef.instance.propertySettingsApplied.subscribe((property) => {
scadaSymbolPropertyPanelPopover.hide();
dynamicFormPropertyPanelPopover.tbComponentRef.instance.popover = dynamicFormPropertyPanelPopover;
dynamicFormPropertyPanelPopover.tbComponentRef.instance.propertySettingsApplied.subscribe((property) => {
dynamicFormPropertyPanelPopover.hide();
this.propertyRowFormGroup.patchValue(
{
id: property.id,
@ -215,7 +224,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
this.modelValue = property;
this.propagateChange(this.modelValue);
});
scadaSymbolPropertyPanelPopover.tbDestroy.subscribe(() => {
dynamicFormPropertyPanelPopover.tbDestroy.subscribe(() => {
if (!propertyValid(this.modelValue)) {
editCanceled();
}
@ -244,7 +253,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
propertyIdNotUnique: true
};
}
const property: ScadaSymbolProperty = {...this.modelValue, ...this.propertyRowFormGroup.value};
const property: FormProperty = {...this.modelValue, ...this.propertyRowFormGroup.value};
if (!propertyValid(property)) {
return {
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.default = defaultPropertyValue(newType);
}
private updateModel() {
const value: ScadaSymbolProperty = this.propertyRowFormGroup.value;
const value: FormProperty = this.propertyRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
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>
</div>
<div [class.!hidden]="!propertyRows?.length && !appearanceProperties.children.length" class="tb-form-panel" formGroupName="properties">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div #appearanceProperties><ng-content select=".tb-scada-symbol-appearance-properties"></ng-content></div>
<div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between flex-wrap overflow-auto" [class]="propertyRow.rowClass">
<mat-slide-toggle *ngIf="propertyRow.switch" 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">
<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>
<tb-dynamic-form formControlName="properties"
title="{{ 'widget-config.appearance' | translate }}"
[properties]="metadata?.properties">
<ng-container class="tb-properties-content"><ng-content select=".tb-scada-symbol-appearance-properties"></ng-content></ng-container>
</tb-dynamic-form>
</ng-container>
<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,
UntypedFormControl,
UntypedFormGroup,
Validator,
ValidatorFn,
Validators
Validator
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -33,19 +31,16 @@ import {
parseScadaSymbolMetadataFromContent,
ScadaSymbolBehaviorType,
ScadaSymbolMetadata,
ScadaSymbolObjectSettings,
ScadaSymbolPropertyType
ScadaSymbolObjectSettings
} from '@home/components/widget/lib/scada/scada-symbol.models';
import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
import { mergeDeep } from '@core/utils';
import {
ScadaSymbolBehaviorGroup,
ScadaSymbolPropertyRow,
toBehaviorGroups,
toPropertyRows
toBehaviorGroups
} 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 { ImageService } from '@core/http/image.service';
import { map } from 'rxjs/operators';
@ -71,8 +66,6 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
ScadaSymbolBehaviorType = ScadaSymbolBehaviorType;
ScadaSymbolPropertyType = ScadaSymbolPropertyType;
@Input()
disabled: boolean;
@ -101,14 +94,10 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
private propagateChange = null;
private validatorTriggers: string[];
private validatorSubscription: Subscription;
public scadaSymbolObjectSettingsFormGroup: UntypedFormGroup;
metadata: ScadaSymbolMetadata;
behaviorGroups: ScadaSymbolBehaviorGroup[];
propertyRows: ScadaSymbolPropertyRow[];
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder,
@ -119,7 +108,7 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
ngOnInit(): void {
this.scadaSymbolObjectSettingsFormGroup = this.fb.group({
behavior: this.fb.group({}),
properties: this.fb.group({})
properties: [{}, []]
});
this.scadaSymbolObjectSettingsFormGroup.valueChanges.subscribe(() => {
this.updateModel();
@ -151,7 +140,6 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
this.scadaSymbolObjectSettingsFormGroup.disable({emitEvent: false});
} else {
this.scadaSymbolObjectSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
@ -170,12 +158,6 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
}
private loadMetadata() {
if (this.validatorSubscription) {
this.validatorSubscription.unsubscribe();
this.validatorSubscription = null;
}
this.validatorTriggers = [];
let metadata$: Observable<ScadaSymbolMetadata>;
if (this.scadaSymbolMetadata) {
metadata$ = of(this.scadaSymbolMetadata);
@ -196,74 +178,19 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
(metadata) => {
this.metadata = metadata;
this.behaviorGroups = toBehaviorGroups(this.metadata.behavior);
this.propertyRows = toPropertyRows(this.metadata.properties);
const behaviorFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('behavior') as UntypedFormGroup;
for (const control of Object.keys(behaviorFormGroup.controls)) {
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) {
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.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() {
if (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 {
ScadaSymbolBehavior,
ScadaSymbolProperty,
ScadaSymbolPropertyType
ScadaSymbolBehavior
} from '@home/components/widget/lib/scada/scada-symbol.models';
export interface ScadaSymbolBehaviorGroup {
@ -25,13 +23,6 @@ export interface ScadaSymbolBehaviorGroup {
behaviors: ScadaSymbolBehavior[];
}
export interface ScadaSymbolPropertyRow {
label: string;
properties: ScadaSymbolProperty[];
switch?: ScadaSymbolProperty;
rowClass?: string;
}
export const toBehaviorGroups = (behaviors: ScadaSymbolBehavior[]): ScadaSymbolBehaviorGroup[] => {
const result: ScadaSymbolBehaviorGroup[] = [];
for (const behavior of behaviors) {
@ -54,24 +45,3 @@ export const toBehaviorGroups = (behaviors: ScadaSymbolBehavior[]): ScadaSymbolB
}
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 {
WidgetButtonToggleCustomStylePanelComponent
} 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({
declarations: [
@ -226,7 +242,13 @@ import {
DataKeyInputComponent,
EntityAliasInputComponent,
AdvancedRangeComponent,
GradientComponent
GradientComponent,
DynamicFormPropertiesComponent,
DynamicFormPropertyRowComponent,
DynamicFormPropertyPanelComponent,
DynamicFormSelectItemsComponent,
DynamicFormSelectItemRowComponent,
DynamicFormComponent
],
imports: [
CommonModule,
@ -293,7 +315,13 @@ import {
DataKeyInputComponent,
EntityAliasInputComponent,
AdvancedRangeComponent,
GradientComponent
GradientComponent,
DynamicFormPropertiesComponent,
DynamicFormPropertyRowComponent,
DynamicFormPropertyPanelComponent,
DynamicFormSelectItemsComponent,
DynamicFormSelectItemRowComponent,
DynamicFormComponent
],
providers: [
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">
<ng-container *ngTemplateOutlet="appearance"></ng-container>
</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="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-title</div>
@ -310,5 +313,17 @@
</tb-widget-settings>
</div>
</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>

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

@ -32,7 +32,7 @@ import {
CellClickColumnInfo,
DataKey,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
datasourcesHasOnlyComparisonAggregation, DynamicFormData,
GroupInfo,
JsonSchema,
JsonSettingsSchema,
@ -190,6 +190,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
public widgetSettings: UntypedFormGroup;
public layoutSettings: UntypedFormGroup;
public advancedSettings: UntypedFormGroup;
public oldAdvancedSettings: UntypedFormGroup;
public actionsSettings: UntypedFormGroup;
private createBasicModeComponentTimeout: Timeout;
@ -203,6 +204,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
private widgetSettingsSubscription: Subscription;
private layoutSettingsSubscription: Subscription;
private advancedSettingsSubscription: Subscription;
private oldAdvancedSettingsSubscription: Subscription;
private actionsSettingsSubscription: Subscription;
private defaultConfigFormsType: widgetType;
@ -221,6 +223,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({});
this.advancedSettings = this.fb.group({});
this.oldAdvancedSettings = this.fb.group({});
this.widgetSettings = this.fb.group({
title: [null, []],
titleFont: [null, []],
@ -296,6 +299,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
this.advancedSettingsSubscription.unsubscribe();
this.advancedSettingsSubscription = null;
}
if (this.oldAdvancedSettingsSubscription) {
this.oldAdvancedSettingsSubscription.unsubscribe();
this.oldAdvancedSettingsSubscription = null;
}
if (this.actionsSettingsSubscription) {
this.actionsSettingsSubscription.unsubscribe();
this.actionsSettingsSubscription = null;
@ -318,6 +325,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe(
() => this.updateAdvancedSettings()
);
this.oldAdvancedSettingsSubscription = this.oldAdvancedSettings.valueChanges.subscribe(
() => this.updateOldAdvancedSettings()
);
this.actionsSettingsSubscription = this.actionsSettings.valueChanges.subscribe(
() => this.updateActionSettings()
);
@ -340,6 +350,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
value: 'appearance'
}
);
this.headerOptions.push(
{
name: 'Old appearance',
value: 'oldAppearance'
}
);
}
this.headerOptions.push(
{
@ -393,6 +409,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
}
this.advancedSettings.addControl('settings',
this.fb.control(null, []));
this.oldAdvancedSettings.addControl('settings',
this.fb.control(null, []));
}
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) {
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 = {};
if (this.modelValue.settingsSchema && 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.settingsDirective = this.modelValue.settingsDirective;
this.advancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false});
this.oldAdvancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false});
}
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() {
if (this.modelValue) {
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 { UtilsService } from '@core/services/utils.service';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
export interface IWidgetAction {
name: string;
@ -577,6 +578,7 @@ export interface WidgetConfigComponentData {
typeParameters: WidgetTypeParameters;
actionSources: {[actionSourceId: string]: WidgetActionSource};
isDataEnabled: boolean;
settingsForm: FormProperty[];
settingsSchema: JsonSettingsSchema;
dataKeySettingsSchema: 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 {
ScadaSymbolBehaviorPanelComponent
} 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 {
ScadaSymbolMetadataTagFunctionPanelComponent
@ -58,10 +49,7 @@ import {
ScadaSymbolMetadataTagFunctionPanelComponent,
ScadaSymbolBehaviorsComponent,
ScadaSymbolBehaviorRowComponent,
ScadaSymbolBehaviorPanelComponent,
ScadaSymbolPropertiesComponent,
ScadaSymbolPropertyRowComponent,
ScadaSymbolPropertyPanelComponent
ScadaSymbolBehaviorPanelComponent
],
imports: [
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>
</div>
<div [class.!hidden]="selectedOption !== 'properties'" class="mat-content overflow-hidden">
<tb-scada-symbol-metadata-properties
<tb-dynamic-form-properties
formControlName="properties">
</tb-scada-symbol-metadata-properties>
</tb-dynamic-form-properties>
</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,
ScadaSymbolBehaviorType,
scadaSymbolContentData,
ScadaSymbolMetadata,
ScadaSymbolProperty,
ScadaSymbolPropertyType
ScadaSymbolMetadata
} from '@home/components/widget/lib/scada/scada-symbol.models';
import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { AceHighlightRule, AceHighlightRules } from '@shared/models/ace/ace.models';
import { HelpLinks, ValueType } from '@shared/models/constants';
import { formPropertyCompletions } from '@shared/models/dynamic-form.models';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
import ITooltipsterHelper = JQueryTooltipster.ITooltipsterHelper;
@ -1141,11 +1140,8 @@ export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags
meta: 'object',
type: 'object',
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 = {
meta: '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 description = customTranslate.transform(value.name, value.name);
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 => {
switch (type) {
case ValueType.STRING:

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

@ -188,6 +188,15 @@
</div>
<div #topRightPanel class="tb-split tb-content">
<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 }}">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonSettingsFullscreen">
<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,
Inject,
OnDestroy,
OnInit, Renderer2,
ViewChild, ViewContainerRef,
OnInit,
Renderer2,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { Store } from '@ngrx/store';
@ -65,12 +67,13 @@ import { Observable } from 'rxjs/internal/Observable';
import { catchError, map, tap } from 'rxjs/operators';
import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models';
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 { TbPopoverService } from '@shared/components/popover.service';
import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component';
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
@Component({
@ -197,6 +200,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private customTranslate: CustomTranslatePipe,
private http: HttpClient) {
super(store);
@ -223,6 +227,9 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
const config = JSON.parse(this.widget.defaultConfig);
this.widget.defaultConfig = JSON.stringify(config);
}
if (!this.widget.settingsForm?.length) {
this.widget.settingsForm = jsonFormSchemaToFormProperties(this.widget.settingsSchema);
}
this.origWidget = deepClone(this.widget);
if (!this.widgetTypeDetails) {
this.isDirty = true;
@ -405,6 +412,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.jsEditor.on('change', () => {
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 || [];
})
));
@ -873,6 +885,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.isDirty = true;
}
settingsFormUpdated() {
this.isDirty = true;
this.updateControllerScriptCompleters();
}
editControllerScriptModules($event: Event, button: MatIconButton) {
if ($event) {
$event.stopPropagation();
@ -951,7 +968,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
const modulesCompleterObservable = loadModulesCompleter(this.http, this.controllerScriptModules);
modulesCompleterObservable.subscribe((modulesCompleter) => {
const completers: Ace.Completer[] = [];
completers.push(widgetEditorCompleter);
const formPropertiesCompletions = formPropertyCompletions(this.widget.settingsForm || [], this.customTranslate);
completers.push(widgetEditorCompleter(formPropertiesCompletions));
if (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 { 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';
const widgetEditorCompletions: TbEditorCompletions = {
... {self: {
description: 'Built-in variable <b>self</b> that is a reference to the widget instance',
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>',
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.',
meta: 'function'
},
onDataUpdated: {
description: 'Called when the new data is available from the widget subscription.<br>Latest data can be accessed from ' +
'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>.',
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>.',
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>.',
meta: 'function'
},
onDestroy: {
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>.',
meta: 'function',
return: {
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>.',
meta: 'function',
return: {
description: 'A particular data key settings schema json',
type: 'object'
}
},
typeParameters: {
description: 'Returns object describing widget datasource parameters.',
meta: 'function',
return: {
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.',
meta: 'function',
return: {
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>}'
}
const widgetEditorCompletions = (settingsCompletions?: TbEditorCompletions): TbEditorCompletions => {
return {
... {self: {
description: 'Built-in variable <b>self</b> that is a reference to the widget instance',
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>',
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.',
meta: 'function'
},
onDataUpdated: {
description: 'Called when the new data is available from the widget subscription.<br>Latest data can be accessed from ' +
'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>.',
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>.',
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>.',
meta: 'function'
},
onDestroy: {
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>.',
meta: 'function',
return: {
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>.',
meta: 'function',
return: {
description: 'A particular data key settings schema json',
type: 'object'
}
},
typeParameters: {
description: 'Returns object describing widget datasource parameters.',
meta: 'function',
return: {
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.',
meta: 'function',
return: {
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 { WidgetTypeAutocompleteComponent } from '@home/pages/widget/widget-type-autocomplete.component';
import { WidgetsBundleDialogComponent } from '@home/pages/widget/widgets-bundle-dialog.component';
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
@NgModule({
declarations: [
@ -47,6 +48,7 @@ import { WidgetsBundleDialogComponent } from '@home/pages/widget/widgets-bundle-
CommonModule,
SharedModule,
HomeComponentsModule,
WidgetConfigComponentsModule,
WidgetLibraryRoutingModule
]
})

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

@ -68,11 +68,16 @@
warning
</mat-icon>
</mat-form-field>
<mat-chip-listbox *ngIf="valueType === valueTypeEnum.BOOLEAN" class="tb-boolean-input center-stretch flex-1"
[disabled]="disabled" name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()">
<mat-chip-option [selectable]="!modelValue" [value]="true">{{ trueLabel }}</mat-chip-option>
<mat-chip-option [selectable]="modelValue" [value]="false">{{ falseLabel }}</mat-chip-option>
</mat-chip-listbox>
<ng-container *ngIf="valueType === valueTypeEnum.BOOLEAN">
<mat-chip-listbox *ngIf="!shortBooleanField" class="tb-boolean-input center-stretch flex-1"
[disabled]="disabled" name="value" #value="ngModel" [(ngModel)]="modelValue" (ngModelChange)="onValueChanged()">
<mat-chip-option [selectable]="!modelValue" [value]="true">{{ trueLabel }}</mat-chip-option>
<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">
<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"

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

@ -36,6 +36,7 @@ import {
import { BreakpointObserver } from '@angular/cdk/layout';
import { Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
type Layout = 'column' | 'row';
@ -67,12 +68,19 @@ export class ValueInputComponent implements OnInit, OnDestroy, OnChanges, Contro
@Input()
valueType: ValueType;
@Input()
allowedValueTypes: ValueType[];
@Input()
trueLabel: string;
@Input()
falseLabel: string;
@Input()
@coerceBoolean()
shortBooleanField = false;
@Input()
layout: ValueInputLayout | Layout = 'row';
@ -110,6 +118,9 @@ export class ValueInputComponent implements OnInit, OnDestroy, OnChanges, Contro
if (!this.falseLabel) {
this.falseLabel = this.translate.instant('value.false');
}
if (this.allowedValueTypes?.length) {
this.valueTypeKeys = this.allowedValueTypes;
}
this._subscription = new Subscription();
this.showValueType = !this.valueType;
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 { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { TbFunction } from '@shared/models/js-function.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
export enum widgetType {
timeseries = 'timeseries',
@ -153,6 +154,7 @@ export interface WidgetTypeDescriptor {
templateHtml: string;
templateCss: string;
controllerScript: TbFunction;
settingsForm?: FormProperty[];
settingsSchema?: string | any;
dataKeySettingsSchema?: string | any;
latestDataKeySettingsSchema?: string | any;
@ -830,6 +832,12 @@ export interface JsonSettingsSchema {
groupInfoes?: GroupInfo[];
}
export interface DynamicFormData {
settingsForm?: FormProperty[];
model?: any;
settingsDirective?: string;
}
export interface WidgetPosition {
row: 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."
}
},
"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-profiles": "Asset profiles",
@ -3099,38 +3146,6 @@
"not-unique-behavior-ids-error": "Behavior Ids must be unique!",
"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": "SCADA symbol",
"fluid-presence": "Fluid presence",
@ -5429,6 +5444,7 @@
"html": "HTML",
"tidy": "Tidy",
"css": "CSS",
"settings-form-properties": "Settings form properties",
"settings-schema": "Settings schema",
"datakey-settings-schema": "Data key settings schema",
"latest-datakey-settings-schema": "Latest data key settings schema",

Loading…
Cancel
Save