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 8e8745dd20..2e69f4adfd 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 @@ -91,7 +91,9 @@ export class AddWidgetDialogComponent extends DialogComponent
- - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts index bb9290a1a1..03d72101f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts @@ -72,6 +72,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @Input() dataKeySettingsSchema: any; + @Input() + dataKeySettingsDirective: string; + @Input() showPostProcessing = true; @@ -121,11 +124,15 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con type: DataKeyType.alarm }); } - if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema) { + if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema || + this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) { this.displayAdvanced = true; this.dataKeySettingsData = { - schema: this.dataKeySettingsSchema.schema, - form: this.dataKeySettingsSchema.form || ['*'] + schema: this.dataKeySettingsSchema?.schema || { + type: 'object', + properties: {} + }, + form: this.dataKeySettingsSchema?.form || ['*'] }; this.dataKeySettingsFormGroup = this.fb.group({ settings: [null, []] diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts index 781c30b02f..63e72905e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts @@ -113,6 +113,9 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie @Input() datakeySettingsSchema: any; + @Input() + dataKeySettingsDirective: string; + @Input() callbacks: DataKeysCallbacks; @@ -395,6 +398,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie data: { dataKey: deepClone(key), dataKeySettingsSchema: this.datakeySettingsSchema, + dataKeySettingsDirective: this.dataKeySettingsDirective, entityAliasId: this.entityAliasId, showPostProcessing: this.widgetType !== widgetType.alarm, callbacks: this.callbacks diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html new file mode 100644 index 0000000000..6efcc3611b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.html @@ -0,0 +1,36 @@ + +
+ + {{ 'widgets.qr-code.use-qr-code-text-function' | translate }} + + + widgets.qr-code.qr-code-text-pattern + + + {{ 'widgets.qr-code.qr-code-text-pattern-required' | translate }} + + +
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts new file mode 100644 index 0000000000..c9ed32b85a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/qrcode-widget-settings.component.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2022 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 } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + + +@Component({ + selector: 'tb-qrcode-widget-settings', + templateUrl: './qrcode-widget-settings.component.html', + styleUrls: [] +}) +export class QrCodeWidgetSettingsComponent extends WidgetSettingsComponent { + + qrCodeWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.qrCodeWidgetSettingsForm; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.qrCodeWidgetSettingsForm = this.fb.group({ + qrCodeTextPattern: [settings ? settings.qrCodeTextPattern : '${entityName}', []], + useQrCodeTextFunction: [settings ? settings.useQrCodeTextFunction : false, []], + qrCodeTextFunction: [settings ? settings.qrCodeTextFunction : 'return data[0][\'entityName\'];', []] + }); + } + + protected validatorTriggers(): string[] { + return ['useQrCodeTextFunction']; + } + + protected updateValidators(emitEvent: boolean) { + const useQrCodeTextFunction: boolean = this.qrCodeWidgetSettingsForm.get('useQrCodeTextFunction').value; + if (useQrCodeTextFunction) { + this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').setValidators([]); + this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([Validators.required]); + } else { + this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').setValidators([Validators.required]); + this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').setValidators([]); + } + this.qrCodeWidgetSettingsForm.get('qrCodeTextPattern').updateValueAndValidity({emitEvent}); + this.qrCodeWidgetSettingsForm.get('qrCodeTextFunction').updateValueAndValidity({emitEvent}); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts new file mode 100644 index 0000000000..7a10d51465 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2022 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 { NgModule, Type } from '@angular/core'; +import { QrCodeWidgetSettingsComponent } from '@home/components/widget/lib/settings/qrcode-widget-settings.component'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; +import { IWidgetSettingsComponent } from '@shared/models/widget.models'; + +@NgModule({ + declarations: [ + QrCodeWidgetSettingsComponent + ], + imports: [ + CommonModule, + SharedModule, + SharedHomeComponentsModule + ], + exports: [ + QrCodeWidgetSettingsComponent + ] +}) +export class WidgetSettingsModule { +} + +export const widgetSettingsComponentsMap: {[key: string]: Type} = { + 'tb-qrcode-widget-settings': QrCodeWidgetSettingsComponent +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index de88b73e3b..851854e331 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -107,6 +107,8 @@ export class WidgetComponentService { controllerScript: this.utils.editWidgetInfo.controllerScript, settingsSchema: this.utils.editWidgetInfo.settingsSchema, dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema, + settingsDirective: this.utils.editWidgetInfo.settingsDirective, + dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective, defaultConfig: this.utils.editWidgetInfo.defaultConfig }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined ); 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 bb4ff8503e..9bfbcc71a9 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 @@ -190,6 +190,7 @@ [optDataKeys]="modelValue?.typeParameters?.dataKeysOptional" [aliasController]="aliasController" [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" + [dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective" [callbacks]="widgetConfigCallbacks" [entityAliasId]="datasourceControl.get('entityAliasId').value" [formControl]="datasourceControl.get('dataKeys')"> @@ -283,6 +284,7 @@ [datasourceType]="alarmSourceSettings.get('type').value" [aliasController]="aliasController" [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" + [dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective" [callbacks]="widgetConfigCallbacks" [entityAliasId]="alarmSourceSettings.get('entityAliasId').value" formControlName="dataKeys"> @@ -476,9 +478,10 @@
- - +
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 c34698bafd..b3325e0de0 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 @@ -605,7 +605,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont widgetSettingsFormData.schema = deepClone(emptySettingsSchema); widgetSettingsFormData.form = deepClone(defaultSettingsForm); widgetSettingsFormData.groupInfoes = deepClone(emptySettingsGroupInfoes); - widgetSettingsFormData.model = {}; + widgetSettingsFormData.model = settings || {}; } this.advancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false}); } @@ -713,7 +713,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont } public displayAdvanced(): boolean { - return !!this.modelValue && !!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema; + return !!this.modelValue && (!!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema || + !!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length); } public dndDatasourceMoved(index: number) { @@ -913,7 +914,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont valid: false } }; - } else if (!this.advancedSettings.valid) { + } else if (!this.advancedSettings.valid || (this.displayAdvanced() && !this.modelValue.config.settings)) { return { advancedSettings: { valid: false diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html new file mode 100644 index 0000000000..a9742c0545 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.html @@ -0,0 +1,24 @@ + +
+ +
{{definedDirectiveError}}
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss new file mode 100644 index 0000000000..2afac0944b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2022 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 { + .tb-settings-directive-error { + font-size: 13px; + font-weight: 400; + color: rgb(221, 44, 0); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts new file mode 100644 index 0000000000..7b5e3f1b0f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/widget-settings.component.ts @@ -0,0 +1,201 @@ +/// +/// Copyright © 2016-2022 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 { + AfterViewInit, + Component, ComponentFactoryResolver, + ComponentRef, + forwardRef, + Input, + OnDestroy, + OnInit, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + IRuleNodeConfigurationComponent, + RuleNodeConfiguration, + RuleNodeDefinition +} from '@shared/models/rule-node.models'; +import { Subscription } from 'rxjs'; +import { RuleChainService } from '@core/http/rule-chain.service'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { TranslateService } from '@ngx-translate/core'; +import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; +import { deepClone } from '@core/utils'; +import { RuleChainType } from '@shared/models/rule-chain.models'; +import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; +import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; +import { IWidgetSettingsComponent, WidgetSettings } from '@shared/models/widget.models'; +import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module'; + +@Component({ + selector: 'tb-widget-settings', + templateUrl: './widget-settings.component.html', + styleUrls: ['./widget-settings.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetSettingsComponent), + multi: true + }] +}) +export class WidgetSettingsComponent implements ControlValueAccessor, OnInit, OnDestroy, AfterViewInit { + + @ViewChild('definedSettingsContent', {read: ViewContainerRef, static: true}) definedSettingsContainer: ViewContainerRef; + + @ViewChild('jsonFormComponent') jsonFormComponent: JsonFormComponent; + + @Input() + disabled: boolean; + + settingsDirectiveValue: string; + + @Input() + set settingsDirective(settingsDirective: string) { + if (this.settingsDirectiveValue !== settingsDirective) { + this.settingsDirectiveValue = settingsDirective; + if (this.settingsDirectiveValue) { + this.validateDefinedDirective(); + } + } + } + + get settingsDirective(): string { + return this.settingsDirectiveValue; + } + + definedDirectiveError: string; + + widgetSettingsFormGroup: FormGroup; + + changeSubscription: Subscription; + + private definedSettingsComponentRef: ComponentRef; + private definedSettingsComponent: IWidgetSettingsComponent; + + private widgetSettingsFormData: JsonFormComponentData; + + private propagateChange = (v: any) => { }; + + constructor(private translate: TranslateService, + private cfr: ComponentFactoryResolver, + private fb: FormBuilder) { + this.widgetSettingsFormGroup = this.fb.group({ + settings: [null, Validators.required] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + if (this.definedSettingsComponentRef) { + this.definedSettingsComponentRef.destroy(); + } + } + + ngAfterViewInit(): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.widgetSettingsFormGroup.disable({emitEvent: false}); + } else { + this.widgetSettingsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: JsonFormComponentData): void { + this.widgetSettingsFormData = value; + if (this.changeSubscription) { + this.changeSubscription.unsubscribe(); + this.changeSubscription = null; + } + if (this.definedSettingsComponent) { + this.definedSettingsComponent.settings = this.widgetSettingsFormData.model; + this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { + this.updateModel(settings); + }); + } else { + this.widgetSettingsFormGroup.get('settings').patchValue(this.widgetSettingsFormData, {emitEvent: false}); + this.changeSubscription = this.widgetSettingsFormGroup.get('settings').valueChanges.subscribe( + (widgetSettingsFormData: JsonFormComponentData) => { + this.updateModel(widgetSettingsFormData.model); + } + ); + } + } + + useDefinedDirective(): boolean { + return this.settingsDirective && + this.settingsDirective.length && !this.definedDirectiveError; + } + + useJsonForm(): boolean { + return !this.settingsDirective || !this.settingsDirective.length; + } + + private updateModel(settings: WidgetSettings) { + this.widgetSettingsFormData.model = settings; + if (this.definedSettingsComponent || this.widgetSettingsFormGroup.valid) { + this.propagateChange(this.widgetSettingsFormData); + } else { + this.propagateChange(null); + } + } + + private validateDefinedDirective() { + if (this.definedSettingsComponentRef) { + this.definedSettingsComponentRef.destroy(); + this.definedSettingsComponentRef = null; + } + if (this.settingsDirective && this.settingsDirective.length) { + const componentType = widgetSettingsComponentsMap[this.settingsDirective]; + if (!componentType) { + this.definedDirectiveError = this.translate.instant('widget-config.settings-component-not-found', + {selector: this.settingsDirective}); + } else { + if (this.changeSubscription) { + this.changeSubscription.unsubscribe(); + this.changeSubscription = null; + } + this.definedSettingsContainer.clear(); + const factory = this.cfr.resolveComponentFactory(componentType); + this.definedSettingsComponentRef = this.definedSettingsContainer.createComponent(factory); + this.definedSettingsComponent = this.definedSettingsComponentRef.instance; + this.definedSettingsComponent.settings = this.widgetSettingsFormData?.model; + this.changeSubscription = this.definedSettingsComponent.settingsChanged.subscribe((settings) => { + this.updateModel(settings); + }); + } + } + } + + validate() { + if (this.useDefinedDirective()) { + this.definedSettingsComponent.validate(); + } + } +} 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 3bb727f4f5..49944cb0e5 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 @@ -424,6 +424,8 @@ export interface WidgetConfigComponentData { isDataEnabled: boolean; settingsSchema: JsonSettingsSchema; dataKeySettingsSchema: JsonSettingsSchema; + settingsDirective: string; + dataKeySettingsDirective: string; } export const MissingWidgetType: WidgetInfo = { @@ -510,6 +512,8 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { controllerScript: widgetTypeEntity.descriptor.controllerScript, settingsSchema: widgetTypeEntity.descriptor.settingsSchema, dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, + settingsDirective: widgetTypeEntity.descriptor.settingsDirective, + dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective, defaultConfig: widgetTypeEntity.descriptor.defaultConfig }; } @@ -536,6 +540,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: controllerScript: widgetInfo.controllerScript, settingsSchema: widgetInfo.settingsSchema, dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema, + settingsDirective: widgetInfo.settingsDirective, + dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective, defaultConfig: widgetInfo.defaultConfig }; return { 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 f3aa24f65c..ba0ded66cf 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 @@ -237,6 +237,18 @@ rows="2" maxlength="255"> {{descriptionInput.value?.length || 0}}/255 + + widget.settings-form-selector + + + + widget.data-key-settings-form-selector + +
diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 703cffcaee..45c8943753 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -25,6 +25,14 @@ import { EntityId } from '@shared/models/id/entity-id'; import * as moment_ from 'moment'; import { EntityDataPageLink, EntityFilter, KeyFilter } from '@shared/models/query/query.models'; import { PopoverPlacement } from '@shared/components/popover.models'; +import { PageComponent } from '@shared/components/page.component'; +import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { AbstractControl, FormGroup } from '@angular/forms'; +import {RuleChainType} from "@shared/models/rule-chain.models"; +import {Observable} from "rxjs"; +import {RuleNodeConfiguration} from "@shared/models/rule-node.models"; export enum widgetType { timeseries = 'timeseries', @@ -143,6 +151,8 @@ export interface WidgetTypeDescriptor { controllerScript: string; settingsSchema?: string | any; dataKeySettingsSchema?: string | any; + settingsDirective?: string; + dataKeySettingsDirective?: string; defaultConfig: string; sizeX: number; sizeY: number; @@ -159,6 +169,7 @@ export interface WidgetTypeParameters { singleEntity?: boolean; warnOnPageDataOverflow?: boolean; ignoreDataUpdateOnIntervalTick?: boolean; + } export interface WidgetControllerDescriptor { @@ -483,6 +494,10 @@ export interface WidgetComparisonSettings { comparisonCustomIntervalValue?: number; } +export interface WidgetSettings { + [key: string]: any; +} + export interface WidgetConfig { title?: string; titleIcon?: string; @@ -512,7 +527,7 @@ export interface WidgetConfig { decimals?: number; noDataDisplayMessage?: string; actions?: {[actionSourceId: string]: Array}; - settings?: any; + settings?: WidgetSettings; alarmSource?: Datasource; alarmStatusList?: AlarmSearchStatus[]; alarmSeverityList?: AlarmSeverity[]; @@ -570,3 +585,113 @@ export interface WidgetSize { sizeX: number; sizeY: number; } + +export interface IWidgetSettingsComponent { + settings: WidgetSettings; + settingsChanged: Observable; + validate(); + [key: string]: any; +} + +@Directive() +// tslint:disable-next-line:directive-class-suffix +export abstract class WidgetSettingsComponent extends PageComponent implements + IWidgetSettingsComponent, OnInit, AfterViewInit { + + settingsValue: WidgetSettings; + + private settingsSet = false; + + set settings(value: WidgetSettings) { + this.settingsValue = value; + if (!this.settingsSet) { + this.settingsSet = true; + this.setupSettings(value); + } else { + this.updateSettings(value); + } + } + + get settings(): WidgetSettings { + return this.settingsValue; + } + + settingsChangedEmitter = new EventEmitter(); + settingsChanged = this.settingsChangedEmitter.asObservable(); + + protected constructor(@Inject(Store) protected store: Store) { + super(store); + } + + ngOnInit() {} + + ngAfterViewInit(): void { + setTimeout(() => { + if (!this.validateSettings()) { + this.settingsChangedEmitter.emit(null); + } + }, 0); + } + + validate() { + this.onValidate(); + } + + protected setupSettings(settings: WidgetSettings) { + this.onSettingsSet(this.prepareInputSettings(settings)); + this.updateValidators(false); + for (const trigger of this.validatorTriggers()) { + const path = trigger.split('.'); + let control: AbstractControl = this.settingsForm(); + for (const part of path) { + control = control.get(part); + } + control.valueChanges.subscribe(() => { + this.updateValidators(true, trigger); + }); + } + this.settingsForm().valueChanges.subscribe((updated: WidgetSettings) => { + this.onSettingsChanged(updated); + }); + } + + protected updateSettings(settings: WidgetSettings) { + this.settingsForm().reset(this.prepareInputSettings(settings), {emitEvent: false}); + this.updateValidators(false); + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + } + + protected validatorTriggers(): string[] { + return []; + } + + protected onSettingsChanged(updated: WidgetSettings) { + this.settingsValue = updated; + if (this.validateSettings()) { + this.settingsChangedEmitter.emit(this.prepareOutputSettings(updated)); + } else { + this.settingsChangedEmitter.emit(null); + } + } + + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + return settings; + } + + protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings { + return settings; + } + + protected validateSettings(): boolean { + return this.settingsForm().valid; + } + + protected onValidate() {} + + protected abstract settingsForm(): FormGroup; + + protected abstract onSettingsSet(settings: WidgetSettings); + +} 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 aab433c992..2ee01bb553 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2985,6 +2985,8 @@ "widget-settings": "Widget settings", "description": "Description", "image-preview": "Image preview", + "settings-form-selector": "Settings form selector", + "data-key-settings-form-selector": "Data key settings form selector", "javascript": "Javascript", "js": "JS", "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", @@ -3151,7 +3153,8 @@ "icon-size": "Icon size", "advanced-settings": "Advanced settings", "data-settings": "Data settings", - "no-data-display-message": "\"No data to display\" alternative message" + "no-data-display-message": "\"No data to display\" alternative message", + "settings-component-not-found": "Settings form component not found for selector '{{selector}}'" }, "widget-type": { "import": "Import widget type", @@ -3266,6 +3269,12 @@ "value": "Value" }, "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", + "qr-code": { + "use-qr-code-text-function": "Use QR code text function", + "qr-code-text-pattern": "QR code text pattern (for ex. '${entityName} | ${keyName} - some text.')", + "qr-code-text-pattern-required": "QR code text pattern is required.", + "qr-code-text-function": "QR code text function" + }, "persistent-table": { "rpc-id": "RPC ID", "message-type": "Message type",