From 2c131f67677e7296eed26131a1c229a12e147930 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 12 Dec 2024 18:02:51 +0200 Subject: [PATCH] Introduce dynamic form properties settings, introduce dynamic form component. Migrate from json schema form to dynamic form. --- .../add-widget-dialog.component.ts | 3 + .../dashboard-page/edit-widget.component.ts | 3 + .../config/widget-settings.component.html | 6 +- .../config/widget-settings.component.ts | 32 +- .../widget/lib/scada/scada-symbol.models.ts | 143 +- .../dynamic-form-properties.component.html} | 26 +- .../dynamic-form-properties.component.scss} | 8 +- .../dynamic-form-properties.component.ts} | 67 +- ...ynamic-form-property-panel.component.html} | 173 ++- ...ynamic-form-property-panel.component.scss} | 10 +- .../dynamic-form-property-panel.component.ts | 212 +++ .../dynamic-form-property-row.component.html} | 10 +- .../dynamic-form-property-row.component.scss} | 2 +- .../dynamic-form-property-row.component.ts} | 95 +- ...ynamic-form-select-item-row.component.html | 37 + ...ynamic-form-select-item-row.component.scss | 23 + .../dynamic-form-select-item-row.component.ts | 172 +++ .../dynamic-form-select-items.component.html | 61 + .../dynamic-form-select-items.component.scss | 43 + .../dynamic-form-select-items.component.ts | 204 +++ .../dynamic-form/dynamic-form.component.html | 117 ++ .../dynamic-form/dynamic-form.component.scss | 22 + .../dynamic-form/dynamic-form.component.ts | 265 ++++ ...cada-symbol-object-settings.component.html | 57 +- .../scada-symbol-object-settings.component.ts | 85 +- .../scada-symbol-object-settings.models.ts | 32 +- .../common/widget-settings-common.module.ts | 32 +- .../widget/widget-config.component.html | 15 + .../widget/widget-config.component.ts | 48 +- .../home/models/widget-component.models.ts | 2 + ...scada-symbol-metadata-components.module.ts | 14 +- .../scada-symbol-metadata.component.html | 4 +- .../scada-symbol-property-panel.component.ts | 136 -- .../scada-symbol-editor.models.ts | 43 +- .../pages/widget/widget-editor.component.html | 9 + .../pages/widget/widget-editor.component.ts | 28 +- .../home/pages/widget/widget-editor.models.ts | 145 +- .../pages/widget/widget-library.module.ts | 2 + .../components/value-input.component.html | 15 +- .../components/value-input.component.ts | 11 + .../models/ace/widget-completion.models.ts | 1184 +++++++++-------- .../app/shared/models/dynamic-form.models.ts | 385 ++++++ ui-ngx/src/app/shared/models/widget.models.ts | 8 + .../assets/locale/locale.constant-en_US.json | 80 +- 44 files changed, 2772 insertions(+), 1297 deletions(-) rename ui-ngx/src/app/modules/home/{pages/scada-symbol/metadata-components/scada-symbol-properties.component.html => components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html} (70%) rename ui-ngx/src/app/modules/home/{pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss => components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.scss} (90%) rename ui-ngx/src/app/modules/home/{pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts => components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts} (75%) rename ui-ngx/src/app/modules/home/{pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html => components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html} (54%) rename ui-ngx/src/app/modules/home/{pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss => components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss} (82%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts rename ui-ngx/src/app/modules/home/{pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html => components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html} (84%) rename ui-ngx/src/app/modules/home/{pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss => components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.scss} (94%) rename ui-ngx/src/app/modules/home/{pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts => components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts} (69%) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts delete mode 100644 ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts create mode 100644 ui-ngx/src/app/shared/models/dynamic-form.models.ts diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index 709159aec3..4617f7a4ed 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/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
{{definedDirectiveError}}
- + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts index d8060d7d0e..6e1131cc3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts +++ b/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; 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: { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 8a782a14a2..d646990351 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/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.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 { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html similarity index 70% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html index 684609937e..72d008b84e 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html +++ b/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. --> -
-
+
+
-
scada.property.id
-
scada.property.name
-
scada.property.type
+
dynamic-form.property.id
+
dynamic-form.property.name
+
dynamic-form.property.type
- - + +
- {{ 'scada.property.no-properties' | translate }} + {{ 'dynamic-form.property.no-properties' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.scss similarity index 90% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.scss index 094ea0668b..d3bc52b22e 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss +++ b/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; } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts similarity index 75% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts index 840302161b..e3c0cd1251 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts +++ b/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; + @ViewChildren(DynamicFormPropertyRowComponent) + propertyRows: QueryList; @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 = []; if (properties) { properties.forEach((property) => { diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html similarity index 54% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html index 94efffca85..5b28248bf2 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html +++ b/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. --> -
-
{{ panelTitle | translate }}
-
+
+
{{ panelTitle | translate }}
+
-
scada.property.id
+
dynamic-form.property.id
-
scada.property.name
+
dynamic-form.property.name
-
scada.property.type
+
dynamic-form.property.group-title
+ + + +
+
+
dynamic-form.property.type
- - {{ scadaSymbolPropertyTypeTranslations.get(type) | translate }} + + {{ formPropertyTypeTranslations.get(type) | translate }}
-
-
scada.property.default-value
- +
dynamic-form.property.number-settings
+
+
dynamic-form.property.min
+ + + + +
dynamic-form.property.max
+ + + + +
dynamic-form.property.step
+ + + +
+
+
+ + + + {{ 'dynamic-form.property.properties' | translate }} + + + + + + + +
+ +
+ + + + {{ 'dynamic-form.property.select-options' | translate }} + + + +
+ + {{ 'dynamic-form.property.enable-multiple-select' | translate }} + +
+ + +
+
+
+
+
+
dynamic-form.property.default-value
+ - - - @@ -66,78 +128,69 @@ [step]="propertyFormGroup.get('step').value" type="number" placeholder="{{ 'widget-config.set' | translate }}"> - - - - + + + + {{ item.label | customTranslate }} + + + + + {{ item.label | customTranslate }} + + +
-
-
scada.property.number-settings
-
-
scada.property.min
- - - - -
scada.property.max
- - - - -
scada.property.step
- - - -
-
-
+
- {{ 'scada.property.value-required' | translate }} + {{ 'dynamic-form.property.value-required' | translate }}
- {{ 'scada.property.advanced-ui-settings' | translate }} + {{ 'dynamic-form.property.advanced-ui-settings' | translate }} -
-
scada.property.sub-label
+
+
dynamic-form.property.sub-label
-
+
- {{ 'scada.property.vertical-divider-after' | translate }} + {{ 'dynamic-form.property.vertical-divider-after' | translate }}
-
-
scada.property.input-field-suffix
+
dynamic-form.property.input-field-suffix
-
scada.property.disable-on-property
+
dynamic-form.property.disable-on-property
@@ -148,20 +201,28 @@
-
scada.property.property-row-classes
+ + +
+
+
dynamic-form.property.property-row-classes
- + {{ clazz }}
-
-
scada.property.property-field-classes
+
+
dynamic-form.property.property-field-classes
- + {{ clazz }} @@ -171,7 +232,7 @@
-
+
@@ -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"> delete diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.scss similarity index 94% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.scss index 5c0b4f0166..7ba115b7fe 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss +++ b/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%; } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts similarity index 69% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts index 3da851063f..c734c91fab 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts +++ b/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; @@ -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); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html new file mode 100644 index 0000000000..5691ba532b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html @@ -0,0 +1,37 @@ + +
+ + + + + +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.scss new file mode 100644 index 0000000000..7d9322262a --- /dev/null +++ b/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%; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts new file mode 100644 index 0000000000..3e43c7a3cc --- /dev/null +++ b/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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html new file mode 100644 index 0000000000..03e1fdcbf5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html @@ -0,0 +1,61 @@ + +
+
+
+
dynamic-form.property.value
+
dynamic-form.property.label
+
+
+
+
+ + +
+ +
+
+
+ +
+
+ +
+
+ + + {{ 'dynamic-form.property.no-options' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.scss new file mode 100644 index 0000000000..450056d2d6 --- /dev/null +++ b/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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts new file mode 100644 index 0000000000..8d3c167df0 --- /dev/null +++ b/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; + + @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) { + 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 = []; + if (items) { + items.forEach((item) => { + selectItemsControls.push(this.fb.control(item, [])); + }); + } + return this.fb.array(selectItemsControls); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html new file mode 100644 index 0000000000..1acdcb6962 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html @@ -0,0 +1,117 @@ + +
+
{{ title }}
+ + + +
+ + + +
{{ propertyGroup.title | customTranslate }}
+
+
+ + + +
+
+ + + +
+
+
+ + + + + + + + + + + + + +
+ + {{ propertyRow.label | customTranslate }} + +
{{ propertyRow.label | customTranslate }}
+
+ + +
{{ property.subLabel | customTranslate }}
+ + +
{{ property.fieldSuffix | customTranslate }}
+
+ + + + {{ item.label | customTranslate }} + + + + + + + + + +
{{ property.fieldSuffix | customTranslate }}
+
+ + + + + + +
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.scss new file mode 100644 index 0000000000..ed4f7d837c --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts new file mode 100644 index 0000000000..ac4348df16 --- /dev/null +++ b/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, + 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); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html index c8a0c5cb2a..41c40f8623 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html @@ -36,58 +36,11 @@
-
-
widget-config.appearance
-
-
- - {{ propertyRow.label | customTranslate }} - -
{{ propertyRow.label | customTranslate }}
-
- -
{{ property.subLabel | customTranslate }}
- - -
{{ property.fieldSuffix | customTranslate }}
-
- - - - - - -
{{ property.fieldSuffix | customTranslate }}
-
- - - - - - -
-
-
-
+ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts index e68788cc92..f6113a9e85 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts +++ b/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, 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; 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[] = []; - 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); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models.ts index b884166cd2..2116093c59 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models.ts +++ b/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; -}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 037096f406..1fc32f8c67 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index b2d1d68ed0..3af9faca69 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -32,6 +32,9 @@
+
+ +
widget-config.card-title
@@ -310,5 +313,17 @@
+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index b6c5e02d2b..c9bbcab5d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/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) { diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 03af49d6d2..31583747bc 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts index 7507b4cdcb..6e3a02330f 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html index c505546286..00a18cc2ab 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html @@ -106,9 +106,9 @@
- - +
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts deleted file mode 100644 index 4c59787c0f..0000000000 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts +++ /dev/null @@ -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; - - @Output() - propertySettingsApplied = new EventEmitter(); - - 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; - } - } -} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts index 71fb7f59a2..9fe747db0a 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts +++ b/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 += ` ${customTranslate.transform(property.subLabel, property.subLabel)}`; - } - 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: diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index 416297a80a..6f77256e9e 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -188,6 +188,15 @@
+ +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index 03b5674268..0a3d711dee 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/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); } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts index 5d4afd93fc..b8175704ca 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts +++ b/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 self that is a reference to the widget instance', - type: 'WidgetTypeInstance', - meta: 'object', - children: { - ...{ - onInit: { - description: 'The first function which is called when widget is ready for initialization.
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.
Latest data can be accessed from ' + - 'the defaultSubscription property of widget context (ctx).', - meta: 'function' - }, - onResize: { - description: 'Called when widget container is resized. Latest width and height can be obtained from widget context (ctx).', - meta: 'function' - }, - onEditModeChanged: { - description: 'Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of widget context (ctx).', - meta: 'function' - }, - onMobileModeChanged: { - description: 'Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of widget context (ctx).', - 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 Settings tab of Settings schema section.', - 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 Data key settings schema of Settings schema section.', - 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: 'WidgetTypeParameters' - } - }, - 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]: WidgetActionSource}' - } +const widgetEditorCompletions = (settingsCompletions?: TbEditorCompletions): TbEditorCompletions => { + return { + ... {self: { + description: 'Built-in variable self that is a reference to the widget instance', + type: 'WidgetTypeInstance', + meta: 'object', + children: { + ...{ + onInit: { + description: 'The first function which is called when widget is ready for initialization.
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.
Latest data can be accessed from ' + + 'the defaultSubscription property of widget context (ctx).', + meta: 'function' + }, + onResize: { + description: 'Called when widget container is resized. Latest width and height can be obtained from widget context (ctx).', + meta: 'function' + }, + onEditModeChanged: { + description: 'Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of widget context (ctx).', + meta: 'function' + }, + onMobileModeChanged: { + description: 'Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of widget context (ctx).', + 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 Settings tab of Settings schema section.', + 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 Data key settings schema of Settings schema section.', + 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: 'WidgetTypeParameters' + } + }, + 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]: WidgetActionSource}' + } + } + }, + ...widgetContextCompletionsWithSettings(settingsCompletions) } - }, - ...widgetContextCompletions - } - }} + }} + } }; -export const widgetEditorCompleter = new TbEditorCompleter(widgetEditorCompletions); +export const widgetEditorCompleter = (settingsCompletions?: TbEditorCompletions): TbEditorCompleter => { + return new TbEditorCompleter(widgetEditorCompletions(settingsCompletions)); +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts index 80438b4f4d..f76be11ba1 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts +++ b/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 ] }) diff --git a/ui-ngx/src/app/shared/components/value-input.component.html b/ui-ngx/src/app/shared/components/value-input.component.html index e4aa511e83..e6138dd321 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.html +++ b/ui-ngx/src/app/shared/components/value-input.component.html @@ -68,11 +68,16 @@ warning - - {{ trueLabel }} - {{ falseLabel }} - + + + {{ trueLabel }} + {{ falseLabel }} + + + {{ modelValue ? trueLabel : falseLabel }} + +
and data used by widget instance.', - meta: 'object', - type: 'WidgetContext', - children: { - ...{ - $container: { - description: 'Container element of the widget.
Can be used to dynamically access or modify widget DOM using jQuery API.', - meta: 'property', - type: 'jQuery Object' - }, - $scope: { - description: 'Reference to the current widget component.
Can be used to access/modify component properties when widget is built using Angular approach.', - meta: 'property', - type: 'IDynamicWidgetComponent' - }, - width: { - description: 'Current width of widget container in pixels.', - meta: 'property', - type: 'number' - }, - height: { - description: 'Current height of widget container in pixels.', - meta: 'property', - type: 'number' - }, - isEdit: { - description: 'Indicates whether the dashboard is in in the view or editing state.', - meta: 'property', - type: 'boolean' - }, - isMobile: { - description: 'Indicates whether the dashboard view is less then 960px width (default mobile breakpoint).', - meta: 'property', - type: 'boolean' - }, - widgetConfig: { - description: 'Common widget configuration containing properties such as color (text color), backgroundColor (widget background color), etc.', - meta: 'property', - type: 'WidgetConfig', - children: { - title: { - description: 'Widget title.', - meta: 'property', - type: 'string' - }, - titleIcon: { - description: 'Widget title icon.', - meta: 'property', - type: 'string' - }, - showTitle: { - description: 'Whether to show widget title.', - meta: 'property', - type: 'boolean' - }, - showTitleIcon: { - description: 'Whether to show widget title icon.', - meta: 'property', - type: 'boolean' - }, - iconColor: { - description: 'Widget title icon color.', - meta: 'property', - type: 'string' - }, - iconSize: { - description: 'Widget title icon size.', - meta: 'property', - type: 'string' - }, - titleTooltip: { - description: 'Widget title tooltip content.', - meta: 'property', - type: 'string' - }, - dropShadow: { - description: 'Enable/disable widget card shadow.', - meta: 'property', - type: 'boolean' - }, - enableFullscreen: { - description: 'Whether to enable fullscreen button on widget.', - meta: 'property', - type: 'boolean' - }, - useDashboardTimewindow: { - description: 'Whether to use dashboard timewindow (applicable for timeseries widgets).', - meta: 'property', - type: 'boolean' - }, - displayTimewindow: { - description: 'Whether to display timewindow (applicable for timeseries widgets).', - meta: 'property', - type: 'boolean' - }, - showLegend: { - description: 'Whether to show legend.', - meta: 'property', - type: 'boolean' - }, - legendConfig: { - description: 'Legend configuration.', - meta: 'property', - type: 'LegendConfig', - children: { - position: { - description: 'Legend position. Possible values: \'top\', \'bottom\', \'left\', \'right\'', - meta: 'property', - type: 'LegendPosition', - }, - direction: { - description: 'Legend direction. Possible values: \'column\', \'row\'', - meta: 'property', - type: 'LegendDirection', - }, - showMin: { - description: 'Whether to display aggregated min values.', - meta: 'property', - type: 'boolean', - }, - showMax: { - description: 'Whether to display aggregated max values.', - meta: 'property', - type: 'boolean', - }, - showAvg: { - description: 'Whether to display aggregated average values.', - meta: 'property', - type: 'boolean', - }, - showTotal: { - description: 'Whether to display aggregated total values.', - meta: 'property', - type: 'boolean', +export const widgetContextCompletionsWithSettings = (settingsCompletions?: TbEditorCompletions): TbEditorCompletions => { + return { + ctx: { + description: 'A reference to widget context that has all necessary API
and data used by widget instance.', + meta: 'object', + type: 'WidgetContext', + children: { + ...{ + $container: { + description: 'Container element of the widget.
Can be used to dynamically access or modify widget DOM using jQuery API.', + meta: 'property', + type: 'jQuery Object' + }, + $scope: { + description: 'Reference to the current widget component.
Can be used to access/modify component properties when widget is built using Angular approach.', + meta: 'property', + type: 'IDynamicWidgetComponent' + }, + width: { + description: 'Current width of widget container in pixels.', + meta: 'property', + type: 'number' + }, + height: { + description: 'Current height of widget container in pixels.', + meta: 'property', + type: 'number' + }, + isEdit: { + description: 'Indicates whether the dashboard is in in the view or editing state.', + meta: 'property', + type: 'boolean' + }, + isMobile: { + description: 'Indicates whether the dashboard view is less then 960px width (default mobile breakpoint).', + meta: 'property', + type: 'boolean' + }, + widgetConfig: { + description: 'Common widget configuration containing properties such as color (text color), backgroundColor (widget background color), etc.', + meta: 'property', + type: 'WidgetConfig', + children: { + title: { + description: 'Widget title.', + meta: 'property', + type: 'string' + }, + titleIcon: { + description: 'Widget title icon.', + meta: 'property', + type: 'string' + }, + showTitle: { + description: 'Whether to show widget title.', + meta: 'property', + type: 'boolean' + }, + showTitleIcon: { + description: 'Whether to show widget title icon.', + meta: 'property', + type: 'boolean' + }, + iconColor: { + description: 'Widget title icon color.', + meta: 'property', + type: 'string' + }, + iconSize: { + description: 'Widget title icon size.', + meta: 'property', + type: 'string' + }, + titleTooltip: { + description: 'Widget title tooltip content.', + meta: 'property', + type: 'string' + }, + dropShadow: { + description: 'Enable/disable widget card shadow.', + meta: 'property', + type: 'boolean' + }, + enableFullscreen: { + description: 'Whether to enable fullscreen button on widget.', + meta: 'property', + type: 'boolean' + }, + useDashboardTimewindow: { + description: 'Whether to use dashboard timewindow (applicable for timeseries widgets).', + meta: 'property', + type: 'boolean' + }, + displayTimewindow: { + description: 'Whether to display timewindow (applicable for timeseries widgets).', + meta: 'property', + type: 'boolean' + }, + showLegend: { + description: 'Whether to show legend.', + meta: 'property', + type: 'boolean' + }, + legendConfig: { + description: 'Legend configuration.', + meta: 'property', + type: 'LegendConfig', + children: { + position: { + description: 'Legend position. Possible values: \'top\', \'bottom\', \'left\', \'right\'', + meta: 'property', + type: 'LegendPosition', + }, + direction: { + description: 'Legend direction. Possible values: \'column\', \'row\'', + meta: 'property', + type: 'LegendDirection', + }, + showMin: { + description: 'Whether to display aggregated min values.', + meta: 'property', + type: 'boolean', + }, + showMax: { + description: 'Whether to display aggregated max values.', + meta: 'property', + type: 'boolean', + }, + showAvg: { + description: 'Whether to display aggregated average values.', + meta: 'property', + type: 'boolean', + }, + showTotal: { + description: 'Whether to display aggregated total values.', + meta: 'property', + type: 'boolean', + } } + }, + timewindow: timewindowCompletion, + mobileHeight: { + description: 'Widget height in mobile mode.', + meta: 'property', + type: 'number' + }, + mobileOrder: { + description: 'Widget order in mobile mode.', + meta: 'property', + type: 'number' + }, + color: { + description: 'Widget text color.', + meta: 'property', + type: 'string' + }, + backgroundColor: { + description: 'Widget background color.', + meta: 'property', + type: 'string' + }, + padding: { + description: 'Widget card padding.', + meta: 'property', + type: 'string' + }, + margin: { + description: 'Widget card margin.', + meta: 'property', + type: 'string' + }, + widgetStyle: { + description: 'Widget element style object.', + meta: 'property', + type: 'object' + }, + titleStyle: { + description: 'Widget title element style object.', + meta: 'property', + type: 'object' + }, + units: { + description: 'Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.', + meta: 'property', + type: 'string' + }, + decimals: { + description: 'Optional property defining how many positions should be used to display decimal part of the value number.', + meta: 'property', + type: 'number' + }, + actions: { + description: 'Map of configured widget actions.', + meta: 'property', + type: 'object' + }, + settings: { + description: 'Widget settings containing widget specific properties according to the defined settings json schema', + meta: 'property', + type: 'object', + children: settingsCompletions + }, + alarmSource: { + description: 'Configured alarm source for alarm widget type.', + meta: 'property', + type: 'Datasource' + }, + alarmSearchStatus: { + description: 'Configured default alarm search status for alarm widget type.', + meta: 'property', + type: 'AlarmSearchStatus' + }, + alarmsPollingInterval: { + description: 'Configured alarms polling interval for alarm widget type.', + meta: 'property', + type: 'number' + }, + alarmsMaxCountLoad: { + description: 'Configured maximum alarms to load for alarm widget type.', + meta: 'property', + type: 'number' + }, + alarmsFetchSize: { + description: 'Configured alarms page size used to load alarms.', + meta: 'property', + type: 'number' + }, + datasources: { + description: 'Array of configured widget datasources.', + meta: 'property', + type: 'Array<Datasource>' } - }, - timewindow: timewindowCompletion, - mobileHeight: { - description: 'Widget height in mobile mode.', - meta: 'property', - type: 'number' - }, - mobileOrder: { - description: 'Widget order in mobile mode.', - meta: 'property', - type: 'number' - }, - color: { - description: 'Widget text color.', - meta: 'property', - type: 'string' - }, - backgroundColor: { - description: 'Widget background color.', - meta: 'property', - type: 'string' - }, - padding: { - description: 'Widget card padding.', - meta: 'property', - type: 'string' - }, - margin: { - description: 'Widget card margin.', - meta: 'property', - type: 'string' - }, - widgetStyle: { - description: 'Widget element style object.', - meta: 'property', - type: 'object' - }, - titleStyle: { - description: 'Widget title element style object.', - meta: 'property', - type: 'object' - }, - units: { - description: 'Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.', - meta: 'property', - type: 'string' - }, - decimals: { - description: 'Optional property defining how many positions should be used to display decimal part of the value number.', - meta: 'property', - type: 'number' - }, - actions: { - description: 'Map of configured widget actions.', - meta: 'property', - type: 'object' - }, - settings: { - description: 'Object holding widget settings according to widget type.', - meta: 'property', - type: 'object' - }, - alarmSource: { - description: 'Configured alarm source for alarm widget type.', - meta: 'property', - type: 'Datasource' - }, - alarmSearchStatus: { - description: 'Configured default alarm search status for alarm widget type.', - meta: 'property', - type: 'AlarmSearchStatus' - }, - alarmsPollingInterval: { - description: 'Configured alarms polling interval for alarm widget type.', - meta: 'property', - type: 'number' - }, - alarmsMaxCountLoad: { - description: 'Configured maximum alarms to load for alarm widget type.', - meta: 'property', - type: 'number' - }, - alarmsFetchSize: { - description: 'Configured alarms page size used to load alarms.', - meta: 'property', - type: 'number' - }, - datasources: { - description: 'Array of configured widget datasources.', - meta: 'property', - type: 'Array<Datasource>' } - } - }, - settings: { - description: 'Widget settings containing widget specific properties according to the defined settings json schema', - meta: 'property', - type: 'object' - }, - datasources: { - description: 'Array of resolved widget datasources.', - meta: 'property', - type: 'Array<Datasource>' - }, - data: { - description: 'Array of latest datasources data.', - meta: 'property', - type: 'Array<DatasourceData>' - }, - timeWindow: { - description: 'Current widget timewindow (applicable for timeseries widgets).', - meta: 'property', - type: 'WidgetTimewindow' - }, - units: { - description: 'Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.', - meta: 'property', - type: 'string' - }, - decimals: { - description: 'Optional property defining how many positions should be used to display decimal part of the value number.', - meta: 'property', - type: 'number' - }, - currentUser: { - description: 'Current user object.', - meta: 'property', - type: 'AuthUser', - children: { - sub: { - description: 'User subject (email).', - meta: 'property', - type: 'string' - }, - scopes: { - description: 'User security scopes.', - meta: 'property', - type: 'Array' - }, - userId: { - description: 'User id.', - meta: 'property', - type: 'string' - }, - firstName: { - description: 'User first name.', - meta: 'property', - type: 'string' - }, - lastName: { - description: 'User last name.', - meta: 'property', - type: 'string' - }, - enabled: { - description: 'Whether is user enabled.', - meta: 'property', - type: 'boolean' - }, - tenantId: { - description: 'Tenant id of the user.', - meta: 'property', - type: 'string' - }, - customerId: { - description: 'Customer id of the user (available when user belongs to specific customer).', - meta: 'property', - type: 'string' - }, - isPublic: { - description: 'Special flag indicating public user.', - meta: 'property', - type: 'boolean' - }, - authority: { - description: 'User authority. Possible values: SYS_ADMIN, TENANT_ADMIN, CUSTOMER_USER', - meta: 'property', - type: 'Authority' + }, + settings: { + description: 'Widget settings containing widget specific properties according to the defined settings json schema', + meta: 'property', + type: 'object', + children: settingsCompletions + }, + datasources: { + description: 'Array of resolved widget datasources.', + meta: 'property', + type: 'Array<Datasource>' + }, + data: { + description: 'Array of latest datasources data.', + meta: 'property', + type: 'Array<DatasourceData>' + }, + timeWindow: { + description: 'Current widget timewindow (applicable for timeseries widgets).', + meta: 'property', + type: 'WidgetTimewindow' + }, + units: { + description: 'Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.', + meta: 'property', + type: 'string' + }, + decimals: { + description: 'Optional property defining how many positions should be used to display decimal part of the value number.', + meta: 'property', + type: 'number' + }, + currentUser: { + description: 'Current user object.', + meta: 'property', + type: 'AuthUser', + children: { + sub: { + description: 'User subject (email).', + meta: 'property', + type: 'string' + }, + scopes: { + description: 'User security scopes.', + meta: 'property', + type: 'Array' + }, + userId: { + description: 'User id.', + meta: 'property', + type: 'string' + }, + firstName: { + description: 'User first name.', + meta: 'property', + type: 'string' + }, + lastName: { + description: 'User last name.', + meta: 'property', + type: 'string' + }, + enabled: { + description: 'Whether is user enabled.', + meta: 'property', + type: 'boolean' + }, + tenantId: { + description: 'Tenant id of the user.', + meta: 'property', + type: 'string' + }, + customerId: { + description: 'Customer id of the user (available when user belongs to specific customer).', + meta: 'property', + type: 'string' + }, + isPublic: { + description: 'Special flag indicating public user.', + meta: 'property', + type: 'boolean' + }, + authority: { + description: 'User authority. Possible values: SYS_ADMIN, TENANT_ADMIN, CUSTOMER_USER', + meta: 'property', + type: 'Authority' + } } - } - }, - hideTitlePanel: { - description: 'Manages visibility of widget title panel. Useful for widget with custom title panels or different states. updateWidgetParams() function must be called after this property change.', - meta: 'property', - type: 'boolean' - }, - widgetTitle: { - description: 'If set, will override configured widget title text. updateWidgetParams() function must be called after this property change.', - meta: 'property', - type: 'string' - }, - detectChanges: { - description: 'Trigger change detection for current widget. Must be invoked when widget HTML template bindings should be updated due to widget data changes.', - meta: 'function' - }, - updateWidgetParams: { - description: 'Updates widget with runtime set properties such as widgetTitle, hideTitlePanel, etc. Must be invoked in order these properties changes take effect.', - meta: 'function' - }, - defaultSubscription: { - description: 'Default widget subscription object contains all subscription information,
including current data, according to the widget type.', - meta: 'property', - type: 'IWidgetSubscription' - }, - timewindowFunctions: { - description: 'Object with timewindow functions used to manage widget data time frame. Can by used by Time-series or Alarm widgets.', - meta: 'property', - type: 'TimewindowFunctions', - children: { - onUpdateTimewindow: { - description: 'This function can be used to update current subscription time frame
to historical one identified by startTimeMs and endTimeMs arguments.', - meta: 'function', - args: [ - { - name: 'startTimeMs', - description: 'Timewindow start time in UTC milliseconds', - type: 'number' - }, - { - name: 'endTimeMs', - description: 'Timewindow end time in UTC milliseconds', - type: 'number' - } - ] - }, - onResetTimewindow: { - description: 'Resets subscription time frame to default defined by widget timewindow component
or dashboard timewindow depending on widget settings.', - meta: 'function' + }, + hideTitlePanel: { + description: 'Manages visibility of widget title panel. Useful for widget with custom title panels or different states. updateWidgetParams() function must be called after this property change.', + meta: 'property', + type: 'boolean' + }, + widgetTitle: { + description: 'If set, will override configured widget title text. updateWidgetParams() function must be called after this property change.', + meta: 'property', + type: 'string' + }, + detectChanges: { + description: 'Trigger change detection for current widget. Must be invoked when widget HTML template bindings should be updated due to widget data changes.', + meta: 'function' + }, + updateWidgetParams: { + description: 'Updates widget with runtime set properties such as widgetTitle, hideTitlePanel, etc. Must be invoked in order these properties changes take effect.', + meta: 'function' + }, + defaultSubscription: { + description: 'Default widget subscription object contains all subscription information,
including current data, according to the widget type.', + meta: 'property', + type: 'IWidgetSubscription' + }, + timewindowFunctions: { + description: 'Object with timewindow functions used to manage widget data time frame. Can by used by Time-series or Alarm widgets.', + meta: 'property', + type: 'TimewindowFunctions', + children: { + onUpdateTimewindow: { + description: 'This function can be used to update current subscription time frame
to historical one identified by startTimeMs and endTimeMs arguments.', + meta: 'function', + args: [ + { + name: 'startTimeMs', + description: 'Timewindow start time in UTC milliseconds', + type: 'number' + }, + { + name: 'endTimeMs', + description: 'Timewindow end time in UTC milliseconds', + type: 'number' + } + ] + }, + onResetTimewindow: { + description: 'Resets subscription time frame to default defined by widget timewindow component
or dashboard timewindow depending on widget settings.', + meta: 'function' + } } - } - }, - controlApi: { - description: 'Object that provides API functions for RPC (Control) widgets.', - meta: 'property', - type: 'RpcApi', - children: { - sendOneWayCommand: { - description: 'Sends one way (without response) RPC command to the device.', - meta: 'function', - args: [ - { - name: 'method', - description: 'RPC method name', - type: 'string' - }, - { - name: 'params', - description: 'RPC method params, custom json object', - type: 'object', - optional: true - }, - { - name: 'timeout', - description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.', - type: 'number', - optional: true - }, - { - name: 'persistent', - description: 'RPC request persistent', - type: 'boolean', - optional: true - }, - { - name: 'persistentPollingInterval', - description: 'Polling interval in milliseconds to get persistent RPC command response', - type: 'number', - optional: true + }, + controlApi: { + description: 'Object that provides API functions for RPC (Control) widgets.', + meta: 'property', + type: 'RpcApi', + children: { + sendOneWayCommand: { + description: 'Sends one way (without response) RPC command to the device.', + meta: 'function', + args: [ + { + name: 'method', + description: 'RPC method name', + type: 'string' + }, + { + name: 'params', + description: 'RPC method params, custom json object', + type: 'object', + optional: true + }, + { + name: 'timeout', + description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.', + type: 'number', + optional: true + }, + { + name: 'persistent', + description: 'RPC request persistent', + type: 'boolean', + optional: true + }, + { + name: 'persistentPollingInterval', + description: 'Polling interval in milliseconds to get persistent RPC command response', + type: 'number', + optional: true + } + ], + return: { + description: 'A command execution Observable.', + type: 'Observable<any>' } - ], - return: { - description: 'A command execution Observable.', - type: 'Observable<any>' - } - }, - sendTwoWayCommand: { - description: 'Sends two way (with response) RPC command to the device.', - meta: 'function', - args: [ - { - name: 'method', - description: 'RPC method name', - type: 'string' - }, - { - name: 'params', - description: 'RPC method params, custom json object', - type: 'object', - optional: true - }, - { - name: 'timeout', - description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.', - type: 'number', - optional: true - }, - { - name: 'persistent', - description: 'RPC request persistent', - type: 'boolean', - optional: true - }, - { - name: 'persistentPollingInterval', - description: 'Polling interval in milliseconds to get persistent RPC command response', - type: 'number', - optional: true + }, + sendTwoWayCommand: { + description: 'Sends two way (with response) RPC command to the device.', + meta: 'function', + args: [ + { + name: 'method', + description: 'RPC method name', + type: 'string' + }, + { + name: 'params', + description: 'RPC method params, custom json object', + type: 'object', + optional: true + }, + { + name: 'timeout', + description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.', + type: 'number', + optional: true + }, + { + name: 'persistent', + description: 'RPC request persistent', + type: 'boolean', + optional: true + }, + { + name: 'persistentPollingInterval', + description: 'Polling interval in milliseconds to get persistent RPC command response', + type: 'number', + optional: true + } + ], + return: { + description: 'A command execution Observable of response body.', + type: 'Observable<any>' } - ], - return: { - description: 'A command execution Observable of response body.', - type: 'Observable<any>' } } - } - }, - actionsApi: { - description: 'Set of API functions to work with user defined actions.', - meta: 'property', - type: 'WidgetActionsApi', - children: { - getActionDescriptors: { - description: 'Get list of action descriptors for provided actionSourceId.', - meta: 'function', - args: [ - { - name: 'actionSourceId', - description: 'Id of widget action source', - type: 'string' + }, + actionsApi: { + description: 'Set of API functions to work with user defined actions.', + meta: 'property', + type: 'WidgetActionsApi', + children: { + getActionDescriptors: { + description: 'Get list of action descriptors for provided actionSourceId.', + meta: 'function', + args: [ + { + name: 'actionSourceId', + description: 'Id of widget action source', + type: 'string' + } + ], + return: { + description: 'The list of action descriptors', + type: 'Array<WidgetActionDescriptor>' } - ], - return: { - description: 'The list of action descriptors', - type: 'Array<WidgetActionDescriptor>' + }, + handleWidgetAction: { + description: 'Handle action produced by particular action source.', + meta: 'function', + args: [ + { + name: '$event', + description: 'DOM event object associated with action.', + type: 'Event' + }, + { + name: 'descriptor', + description: 'An action descriptor.', + type: 'WidgetActionDescriptor' + }, + { + name: 'entityId', + description: 'Current entity id provided by action source if available.', + type: entityIdHref, + optional: true + }, + { + name: 'entityName', + description: 'Current entity name provided by action source if available.', + type: 'string', + optional: true + } + ] } - }, - handleWidgetAction: { - description: 'Handle action produced by particular action source.', - meta: 'function', - args: [ - { - name: '$event', - description: 'DOM event object associated with action.', - type: 'Event' - }, - { - name: 'descriptor', - description: 'An action descriptor.', - type: 'WidgetActionDescriptor' - }, - { - name: 'entityId', - description: 'Current entity id provided by action source if available.', - type: entityIdHref, - optional: true - }, - { - name: 'entityName', - description: 'Current entity name provided by action source if available.', - type: 'string', - optional: true - } - ] } - } - }, - stateController: { - description: 'Reference to Dashboard state controller, providing API to manage current dashboard state.', - meta: 'property', - type: 'IStateController', - children: { - openState: { - description: 'Navigate to new dashboard state.', - meta: 'function', - args: [ - { - name: 'id', - description: 'An id of the target dashboard state.', + }, + stateController: { + description: 'Reference to Dashboard state controller, providing API to manage current dashboard state.', + meta: 'property', + type: 'IStateController', + children: { + openState: { + description: 'Navigate to new dashboard state.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An id of the target dashboard state.', + type: 'string' + }, + { + name: 'params', + description: 'An object with state parameters to use by the new state.', + type: 'StateParams', + optional: true + }, + { + name: 'openRightLayout', + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', + type: 'boolean', + optional: true + } + ] + }, + pushAndOpenState: { + description: 'Navigate to new dashboard state and adding intermediate states.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An array state object of the target dashboard state.', + type: 'Array StateObject', + }, + { + name: 'openRightLayout', + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', + type: 'boolean', + optional: true + } + ] + }, + updateState: { + description: 'Updates current dashboard state.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An optional id of the target dashboard state to replace current state id.', + type: 'string', + optional: true + }, + { + name: 'params', + description: 'An object with state parameters to update current state parameters.', + type: 'StateParams', + optional: true + }, + { + name: 'openRightLayout', + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', + type: 'boolean', + optional: true + } + ] + }, + getStateId: { + description: 'Get current dashboard state id.', + meta: 'function', + return: { + description: 'current dashboard state id.', type: 'string' - }, - { - name: 'params', - description: 'An object with state parameters to use by the new state.', - type: 'StateParams', - optional: true - }, - { - name: 'openRightLayout', - description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', - type: 'boolean', - optional: true } - ] - }, - pushAndOpenState: { - description: 'Navigate to new dashboard state and adding intermediate states.', - meta: 'function', - args: [ - { - name: 'id', - description: 'An array state object of the target dashboard state.', - type: 'Array StateObject', - }, - { - name: 'openRightLayout', - description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', - type: 'boolean', - optional: true + }, + getStateParams: { + description: 'Get current dashboard state parameters.', + meta: 'function', + return: { + description: 'current dashboard state parameters.', + type: 'StateParams' } - ] - }, - updateState: { - description: 'Updates current dashboard state.', - meta: 'function', - args: [ - { - name: 'id', - description: 'An optional id of the target dashboard state to replace current state id.', - type: 'string', - optional: true - }, - { - name: 'params', - description: 'An object with state parameters to update current state parameters.', - type: 'StateParams', - optional: true - }, - { - name: 'openRightLayout', - description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', - type: 'boolean', - optional: true + }, + getStateParamsByStateId: { + description: 'Get state parameters for particular dashboard state identified by id.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An id of the target dashboard state.', + type: 'string' + } + ], + return: { + description: 'current dashboard state parameters.', + type: 'StateParams' } - ] - }, - getStateId: { - description: 'Get current dashboard state id.', - meta: 'function', - return: { - description: 'current dashboard state id.', - type: 'string' - } - }, - getStateParams: { - description: 'Get current dashboard state parameters.', - meta: 'function', - return: { - description: 'current dashboard state parameters.', - type: 'StateParams' - } - }, - getStateParamsByStateId: { - description: 'Get state parameters for particular dashboard state identified by id.', - meta: 'function', - args: [ - { - name: 'id', - description: 'An id of the target dashboard state.', - type: 'string' - } - ], - return: { - description: 'current dashboard state parameters.', - type: 'StateParams' } } } - } - }, - ...serviceCompletions + }, + ...serviceCompletions + } } } }; + +export const widgetContextCompletions: TbEditorCompletions = widgetContextCompletionsWithSettings(); diff --git a/ui-ngx/src/app/shared/models/dynamic-form.models.ts b/ui-ngx/src/app/shared/models/dynamic-form.models.ts new file mode 100644 index 0000000000..70c70b42b5 --- /dev/null +++ b/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.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 += ` ${customTranslate.transform(property.subLabel, property.subLabel)}`; + } + if (property.type === FormPropertyType.select) { + if (property.multiple) { + description += '

Possible values of array element:'; + } else { + description += '

Possible values:'; + } + description += `
    ${property.items.map(item => `
  • ${item.value} ${typeof item.value}
  • `).join('\n')}
`; + } + const completion: TbEditorCompletion = { + meta: 'property', + description, + type: formPropertyCompletionType(property) + }; + if (property.type === FormPropertyType.fieldset) { + completion.children = {}; + for (const childProperty of property.properties) { + completion.children[childProperty.id] = formPropertyCompletion(childProperty, customTranslate); + } + } + return completion; +}; + +const formPropertyCompletionType = (property: FormProperty): string => { + switch (property.type) { + case FormPropertyType.text: + return 'string'; + case FormPropertyType.number: + return 'number'; + case FormPropertyType.switch: + return 'boolean'; + case FormPropertyType.select: + const items = property.items || []; + const types: string[] = []; + items.forEach(item => { + const type = typeof item.value; + if (!types.includes(type)) { + types.push(type); + } + }); + const typesString = types.length ? types.join(' | ') : 'string'; + if (property.multiple) { + return `Array<${typesString}>`; + } else { + return typesString; + } + case FormPropertyType.color: + return 'color string'; + case FormPropertyType.color_settings: + return 'ColorProcessor'; + case FormPropertyType.font: + return 'Font'; + case FormPropertyType.units: + return 'units string'; + case FormPropertyType.icon: + return 'icon string'; + case FormPropertyType.fieldset: + return 'object'; + } +}; + + +export const jsonFormSchemaToFormProperties = (rawSchema: string | any) : FormProperty[] => { + const properties: FormProperty[] = []; + let settingsSchema: JsonSettingsSchema; + if (!rawSchema || rawSchema === '') { + settingsSchema = {}; + } else { + settingsSchema = isString(rawSchema) ? JSON.parse(rawSchema) : rawSchema; + } + if (settingsSchema.schema) { + const schema = settingsSchema.schema; + const form = settingsSchema.form || ['*']; + const groupInfoes = settingsSchema.groupInfoes || []; + if (form.length > 0) { + if (groupInfoes.length) { + for (const info of groupInfoes) { + const theForm: any[] = form[info.formIndex]; + properties.push(...schemaFormToProperties(schema, theForm, info.GroupTitle)); + } + } else { + properties.push(...schemaFormToProperties(schema, form)); + } + } + } + return properties; +} + +const schemaFormToProperties = (schema: JsonSchema, theForm: any[], groupTitle?: string): FormProperty[] => { + const merged = JsonFormUtils.merge(schema, theForm, {}, { + formDefaults: { + startEmpty: true + } + }); + return merged.map((form: JsonFormData) => jsonFormDataToProperty(form, 0, groupTitle)).filter(p => p != null); +} + +const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: string): FormProperty => { + if (form.key && form.key.length > level) { + const property: FormProperty = { + id: form.key[level] + '', + name: form.title, + group: groupTitle, + type: null, + default: form.schema.default, + required: form.required + }; + if (form.condition?.length) { + property.condition = `return ${form.condition};`; + } + switch (form.type) { + case 'number': + property.type = FormPropertyType.number; + break; + case 'text': + property.type = FormPropertyType.text; + property.fieldClass = 'flex'; + break; + case 'checkbox': + property.type = FormPropertyType.switch; + break; + case 'rc-select': + property.type = FormPropertyType.select; + if (form.items?.length) { + property.items = (form.items as KeyLabelItem[]).map(item => ({value: item.value, label: item.label})); + } else { + property.items = []; + } + property.multiple = form.multiple; + property.fieldClass = 'flex'; + break; + case 'select': + property.type = FormPropertyType.select; + if (form.titleMap?.length) { + property.items = form.titleMap.map(item => ({value: item.value, label: item.name})); + } else { + property.items = []; + } + property.multiple = false; + property.fieldClass = 'flex'; + break; + case 'color': + property.type = FormPropertyType.color; + break; + case 'icon': + property.type = FormPropertyType.icon; + break; + case 'fieldset': + property.type = FormPropertyType.fieldset; + property.properties = form.items ? (form.items as JsonFormData[]).map(item => + jsonFormDataToProperty(item, level+1)).filter(p => p !== null) : []; + break; + } + if (!property.type) { + return null; + } + return property; + } + return null; +} diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 37ea1c1607..6f5579c77f 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/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; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index c2f45da84f..c43ff76da3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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",