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 4617f7a4ed..860825f5e8 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 @@ -109,7 +109,8 @@ export class AddWidgetDialogComponent extends DialogComponent diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html index 2b4ed91214..bb299bc4bc 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html @@ -101,8 +101,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html index c5b19aac1f..bc05db8c46 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html @@ -47,8 +47,7 @@ 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 d646990351..16ac07e6fd 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,7 +65,12 @@ 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'; +import { + defaultFormPropertyValue, + defaultPropertyValue, + FormProperty, + FormPropertyType +} from '@shared/models/dynamic-form.models'; export interface ScadaSymbolApi { generateElementId: () => string; @@ -630,7 +635,9 @@ export class ScadaSymbolObject { elements.push(element); } for (const property of this.metadata.properties) { - this.context.properties[property.id] = this.getPropertyValue(property.id); + if (property.type !== FormPropertyType.htmlSection) { + this.context.properties[property.id] = this.getPropertyValue(this.metadata.properties, this.settings.properties, property.id); + } } for (const tag of this.metadata.tags) { if (tag.actions) { @@ -967,9 +974,8 @@ export class ScadaSymbolObject { return Array.isArray(element) ? element : [element]; } - private getProperty(...ids: string[]): FormProperty { + private getProperty(properties: FormProperty[], ...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); @@ -985,9 +991,9 @@ export class ScadaSymbolObject { return found; } - private getSettingsValue(...ids: string[]): any { + private getSettingsValue(settings: {[id: string]: any}, ...ids: string[]): any { let found: any; - let properties = this.settings.properties; + let properties = settings; for (const id of ids) { if (properties) { found = properties[id]; @@ -1003,43 +1009,72 @@ export class ScadaSymbolObject { return found; } - private getPropertyValue(...ids: string[]): any { - const property = this.getProperty(...ids); + private getPropertyValue(properties: FormProperty[], settings: {[id: string]: any}, ...ids: string[]): any { + const property = this.getProperty(properties, ...ids); if (property) { - if (property.type === FormPropertyType.fieldset) { + if (property.type === FormPropertyType.array) { + const arrayValue = []; + if (property.arrayItemType !== FormPropertyType.htmlSection) { + const settingsValue = this.getSettingsValue(settings, ...ids); + if (settingsValue && Array.isArray(settingsValue)) { + for (const settingsElement of settingsValue) { + let value: any; + if (property.arrayItemType === FormPropertyType.fieldset) { + const propertyValue: {[id: string]: any} = {}; + for (const childProperty of property.properties) { + if (childProperty.type !== FormPropertyType.htmlSection) { + propertyValue[childProperty.id] = this.getPropertyValue(property.properties, settingsElement, childProperty.id); + } + } + value = propertyValue; + } else { + value = this.convertPropertyValue(property, settingsElement); + } + arrayValue.push(value); + } + } + } + return arrayValue; + } else if (property.type === FormPropertyType.fieldset) { const propertyValue: {[id: string]: any} = {}; for (const childProperty of property.properties) { - propertyValue[childProperty.id] = this.getPropertyValue(...ids, childProperty.id); + if (childProperty.type !== FormPropertyType.htmlSection) { + propertyValue[childProperty.id] = this.getPropertyValue(properties, settings, ...ids, childProperty.id); + } } return propertyValue; } else { - 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')); - } - } + const value = this.getSettingsValue(settings, ...ids); + return this.convertPropertyValue(property, value); } } else { return ''; } } + + private convertPropertyValue(property: FormProperty, value: any): any { + if (isDefinedAndNotNull(value)) { + if (property.type === FormPropertyType.color_settings) { + return ColorProcessor.fromSettings(value); + } else if ([FormPropertyType.text, FormPropertyType.textarea].includes(property.type)) { + 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.color_settings: + return ColorProcessor.fromSettings(constantColor('#000')); + case FormPropertyType.font: + case FormPropertyType.units: + case FormPropertyType.icon: + return null; + default: + return defaultPropertyValue(property.type); + } + } + } } const scadaSymbolAnimationId = 'scadaSymbolAnimation'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html new file mode 100644 index 0000000000..b15964a5af --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html @@ -0,0 +1,74 @@ + +
+ + + +
{{ title }}
+
+
+ +
+
+
+
{{$index + 1}}
+
+ + +
+ + +
+
+
+
+ +
+
+
+
+ + + {{ 'dynamic-form.property.no-items' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.scss new file mode 100644 index 0000000000..1935e29a26 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.scss @@ -0,0 +1,72 @@ +/** + * 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 "../../../../../../../../../scss/constants"; + +.tb-dynamic-form-array { + .tb-dynamic-form-array-body { + display: flex; + flex-direction: column; + gap: 12px; + } +} +.tb-dynamic-form-array-row { + background: #fff; + display: flex; + flex-direction: row; + place-content: center flex-start; + align-items: flex-start; + .tb-dynamic-form-item-index-container { + width: 36px; + height: 56px; + display: flex; + flex-direction: row; + place-content: flex-start; + align-items: center; + .tb-dynamic-form-item-index { + width: 24px; + height: 24px; + position: relative; + font-weight: 400; + font-size: 16px; + display: flex; + align-items: center; + place-content: center; + color: $tb-primary-color; + &:before { + content: ""; + display: block; + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + background-color: $tb-primary-color; + opacity: 0.06; + border-radius: 100%; + } + } + } + .tb-dynamic-form-array-buttons { + display: flex; + flex-direction: row; + button.mat-mdc-icon-button.mat-mdc-button-base { + color: rgba(0, 0, 0, 0.38); + &.tb-hidden { + visibility: hidden; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts new file mode 100644 index 0000000000..081adbfcea --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts @@ -0,0 +1,161 @@ +/// +/// 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, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { defaultFormPropertyValue, FormProperty } from '@shared/models/dynamic-form.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; + +@Component({ + selector: 'tb-dynamic-form-array', + templateUrl: './dynamic-form-array.component.html', + styleUrls: ['./dynamic-form-array.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DynamicFormArrayComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DynamicFormArrayComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class DynamicFormArrayComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + @Input() + itemProperty: FormProperty; + + @Input() + title: string; + + propertiesFormGroup: UntypedFormGroup; + + get dragEnabled(): boolean { + return !this.disabled && this.propertiesFormArray().controls.length > 1; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.propertiesFormGroup = this.fb.group({ + properties: this.fb.array([]) + }); + this.propertiesFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + const properties: {[id: string]: any}[] = this.propertiesFormGroup.get('properties').value; + const value = properties.map(prop => prop[this.itemProperty.id]); + this.propagateChange(value); + } + ); + } + + 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}); + } + } + + writeValue(values: any[] | undefined): void { + this.propertiesFormGroup.setControl('properties', this.preparePropertiesFormArray(values || []), {emitEvent: false}); + } + + public validate(_c: UntypedFormControl) { + const valid = this.propertiesFormGroup.valid; + return valid ? null : { + properties: { + valid: false, + }, + }; + } + + propertyDrop(event: CdkDragDrop) { + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + const property = propertiesArray.at(event.previousIndex); + propertiesArray.removeAt(event.previousIndex, {emitEvent: false}); + propertiesArray.insert(event.currentIndex, property, {emitEvent: true}); + } + + propertiesFormArray(): UntypedFormArray { + return this.propertiesFormGroup.get('properties') as UntypedFormArray; + } + + trackByProperty(_index: number, propertyControl: AbstractControl): any { + return propertyControl; + } + + removeProperty(index: number, emitEvent = true) { + (this.propertiesFormGroup.get('properties') as UntypedFormArray).removeAt(index, {emitEvent}); + } + + addProperty() { + const property = { + [this.itemProperty.id]: defaultFormPropertyValue(this.itemProperty) + }; + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + const propertyControl = this.fb.control(property, []); + propertiesArray.push(propertyControl); + setTimeout(() => { + propertyControl.updateValueAndValidity(); + }); + } + + private preparePropertiesFormArray(values: any[] | undefined): UntypedFormArray { + const propertiesControls: Array = []; + if (values) { + values.forEach((value) => { + const property = { + [this.itemProperty.id]: value + }; + propertiesControls.push(this.fb.control(property, [])); + }); + } + return this.fb.array(propertiesControls); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html index 72d008b84e..cfdcfacfc4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html @@ -28,6 +28,7 @@ [cdkDropListDisabled]="!dragEnabled" (cdkDropListDropped)="propertyDrop($event)">
p.type === FormPropertyType.switch).map(p => p.id); } - public validate(c: UntypedFormControl) { + public validate(_c: UntypedFormControl) { this.errorText = ''; const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; const notUniqueControls = @@ -189,15 +188,15 @@ export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnI propertyDrop(event: CdkDragDrop) { const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; const property = propertiesArray.at(event.previousIndex); - propertiesArray.removeAt(event.previousIndex); - propertiesArray.insert(event.currentIndex, property); + propertiesArray.removeAt(event.previousIndex, {emitEvent: false}); + propertiesArray.insert(event.currentIndex, property, {emitEvent: true}); } propertiesFormArray(): UntypedFormArray { return this.propertiesFormGroup.get('properties') as UntypedFormArray; } - trackByProperty(index: number, propertyControl: AbstractControl): any { + trackByProperty(_index: number, propertyControl: AbstractControl): any { return propertyControl; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html index 5b28248bf2..c10aaaf803 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html @@ -46,7 +46,139 @@
-
+
+
dynamic-form.property.array-item
+
+
dynamic-form.property.item-type
+ + + + {{ formPropertyTypeTranslations.get(type) | translate }} + + + +
+
+
dynamic-form.property.item-name
+ + + +
+ + +
+ + + + +
+ + {{ 'dynamic-form.property.value-required' | translate }} + +
+
+ + + + {{ 'dynamic-form.property.advanced-ui-settings' | translate }} + + + + +
+
dynamic-form.property.sub-label
+ + + +
+
+ + {{ 'dynamic-form.property.vertical-divider-after' | translate }} + +
+
+
+
dynamic-form.property.input-field-suffix
+ + + +
+
+
dynamic-form.property.disable-on-property
+ + + + + {{ prop }} + + + +
+
+ + +
+ + + + +
+
+
+
+
+ + +
+ + + + +
+
dynamic-form.property.textarea-rows
+ + + +
+
dynamic-form.property.number-settings
dynamic-form.property.min
@@ -68,7 +200,7 @@
-
+
@@ -81,20 +213,60 @@
- +
+ + + + {{ 'dynamic-form.property.html-section-settings' | translate }} + + + +
+ + +
+ + +
+
+
+
- {{ 'dynamic-form.property.select-options' | translate }} + {{ (propertyFormGroup.get('type').value === FormPropertyType.select ? 'dynamic-form.property.select-options' : 'dynamic-form.property.radio-button-options') | translate }} -
+
+
dynamic-form.property.buttons-direction
+ + {{ 'dynamic-form.property.direction-column' | translate }} + {{ 'dynamic-form.property.direction-row' | translate }} + +
+
{{ 'dynamic-form.property.enable-multiple-select' | translate }}
+
+ + {{ 'dynamic-form.property.allow-empty-select-option' | translate }} + +
@@ -102,23 +274,113 @@
-
+
+
dynamic-form.property.help-id
+ + + +
+ +
+
dynamic-form.property.datetime-type
+ + {{ 'dynamic-form.property.datetime-type-date' | translate }} + {{ 'dynamic-form.property.datetime-type-time' | translate }} + {{ 'dynamic-form.property.datetime-type-datetime' | translate }} + +
+
+ + {{ 'dynamic-form.property.enable-clear-button' | translate }} + +
+
+
+ + dynamic-form.property.default-value + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
dynamic-form.property.default-value
+ + +
{{ item.label | customTranslate }}
+
+
+
+
dynamic-form.property.default-value
- - + - - - @@ -128,23 +390,24 @@ [step]="propertyFormGroup.get('step').value" type="number" placeholder="{{ 'widget-config.set' | translate }}"> - - - - - + {{ item.label | customTranslate }} @@ -155,97 +418,38 @@ + + + + +
-
- - {{ 'dynamic-form.property.value-required' | translate }} - -
-
- - - - {{ 'dynamic-form.property.advanced-ui-settings' | translate }} - - - -
-
dynamic-form.property.sub-label
- - - -
-
- - {{ 'dynamic-form.property.vertical-divider-after' | translate }} - -
-
-
dynamic-form.property.input-field-suffix
- - - -
-
-
dynamic-form.property.disable-on-property
- - - - - {{ prop }} - - - -
-
- - -
-
-
dynamic-form.property.property-row-classes
- - - - {{ clazz }} - - - -
-
-
dynamic-form.property.property-field-classes
- - - - {{ clazz }} - - - -
-
-
-
-
-
- - -
-
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss index 1d093f6a22..da613d5e61 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss @@ -46,4 +46,25 @@ justify-content: flex-end; align-items: flex-end; } + + .tb-form-row.tb-radios-property { + &.direction-column { + flex-direction: column; + align-items: flex-start; + gap: 4px; + } + .mat-mdc-radio-group { + overflow: hidden; + .mat-mdc-radio-button { + overflow: hidden; + .mdc-form-field { + overflow: hidden; + width: 100%; + .mdc-label { + overflow: hidden; + } + } + } + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts index a9cb9255a3..ee19d25a3d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts @@ -17,20 +17,21 @@ import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { ValueType } from '@shared/models/constants'; +import { ContentType, ValueType } from '@shared/models/constants'; import { + defaultPropertyValue, FormProperty, formPropertyFieldClasses, formPropertyRowClasses, FormPropertyType, formPropertyTypes, formPropertyTypeTranslations, - FormSelectItem + FormSelectItem, + isPropertyTypeAllowedForRow } from '@shared/models/dynamic-form.models'; -import { - defaultPropertyValue -} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { isUndefinedOrNull } from '@core/utils'; +import { StringItemsOption } from '@shared/components/string-items-list.component'; @Component({ selector: 'tb-dynamic-form-property-panel', @@ -44,12 +45,29 @@ export class DynamicFormPropertyPanelComponent implements OnInit { FormPropertyType = FormPropertyType; + ContentType = ContentType; + formPropertyTypes = formPropertyTypes; + arrayItemFormPropertyTypes = formPropertyTypes.filter(t => t !== FormPropertyType.array); formPropertyTypeTranslations = formPropertyTypeTranslations; - formPropertyRowClasses = formPropertyRowClasses; + formPropertyRowClasses: StringItemsOption[] = formPropertyRowClasses.map(clazz => ({name: clazz, value: clazz})); + + formPropertyFieldClasses: StringItemsOption[] = formPropertyFieldClasses.map(clazz => ({name: clazz, value: clazz})); - formPropertyFieldClasses = formPropertyFieldClasses; + isPropertyTypeAllowedForRow = isPropertyTypeAllowedForRow; + + get propertyItemType(): FormPropertyType | any { + if (this.isArray) { + return this.propertyFormGroup.get('arrayItemType').value; + } else { + return this.propertyFormGroup.get('type').value; + } + } + + get isArray(): boolean { + return this.propertyFormGroup.get('type').value === FormPropertyType.array; + } @Input() isAdd = false; @@ -81,13 +99,15 @@ export class DynamicFormPropertyPanelComponent implements OnInit { ngOnInit(): void { this.panelTitle = this.isAdd ? 'dynamic-form.property.add-property' : 'dynamic-form.property.property-settings'; - this.propertyType = this.property.type; + this.propertyType = this.property.type === FormPropertyType.array ? this.property.arrayItemType : this.property.type; this.propertyFormGroup = this.fb.group( { id: [this.property.id, [Validators.required]], name: [this.property.name, [Validators.required]], group: [this.property.group, []], type: [this.property.type, [Validators.required]], + arrayItemType: [this.property.arrayItemType, [Validators.required]], + arrayItemName: [this.property.arrayItemName, []], default: [this.property.default, []], required: [this.property.required, []], subLabel: [this.property.subLabel, []], @@ -95,14 +115,22 @@ export class DynamicFormPropertyPanelComponent implements OnInit { fieldSuffix: [this.property.fieldSuffix, []], disableOnProperty: [this.property.disableOnProperty, []], condition: [this.property.condition, []], - rowClass: [(this.property.rowClass || '').split(' '), []], - fieldClass: [(this.property.fieldClass || '').split(' '), []], + rowClass: [this.property.rowClass ? this.property.rowClass.split(' ') : [], []], + fieldClass: [this.property.fieldClass ? this.property.fieldClass.split(' ') : [], []], + rows: [this.property.rows, [Validators.min(1)]], min: [this.property.min, []], max: [this.property.max, []], step: [this.property.step, [Validators.min(0)]], properties: [this.property.properties, []], multiple: [this.property.multiple, []], - items: [this.property.items, []] + allowEmptyOption: [this.property.allowEmptyOption, []], + items: [this.property.items, []], + helpId: [this.property.helpId, []], + direction: [this.property.direction || 'column', []], + allowClear: [this.property.allowClear || true, []], + dateTimeType: [this.property.dateTimeType || 'datetime', []], + htmlContent: [this.property.htmlContent || '', []], + htmlClassList: [this.property.htmlClassList || [], []] } ); if (this.disabled) { @@ -113,7 +141,11 @@ export class DynamicFormPropertyPanelComponent implements OnInit { ).subscribe(() => { this.updateValidators(); }); - + this.propertyFormGroup.get('arrayItemType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateValidators(); + }); this.propertyFormGroup.get('items').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe(() => { @@ -140,7 +172,19 @@ export class DynamicFormPropertyPanelComponent implements OnInit { } private updateValidators() { - const type: FormPropertyType = this.propertyFormGroup.get('type').value; + if (this.isArray) { + this.propertyFormGroup.get('arrayItemType').enable({emitEvent: false}); + this.propertyFormGroup.get('arrayItemName').enable({emitEvent: false}); + } else { + this.propertyFormGroup.get('arrayItemType').disable({emitEvent: false}); + this.propertyFormGroup.get('arrayItemName').disable({emitEvent: false}); + } + const type = this.propertyItemType; + if (type === FormPropertyType.textarea) { + this.propertyFormGroup.get('rows').enable({emitEvent: false}); + } else { + this.propertyFormGroup.get('rows').disable({emitEvent: false}); + } if (type === FormPropertyType.number) { this.propertyFormGroup.get('min').enable({emitEvent: false}); this.propertyFormGroup.get('max').enable({emitEvent: false}); @@ -157,27 +201,73 @@ export class DynamicFormPropertyPanelComponent implements OnInit { this.propertyFormGroup.get('default').enable({emitEvent: false}); this.propertyFormGroup.get('properties').disable({emitEvent: false}); } - if (type === FormPropertyType.select) { - this.propertyFormGroup.get('multiple').enable({emitEvent: false}); + if ([FormPropertyType.select, FormPropertyType.radios].includes(type)) { this.propertyFormGroup.get('items').enable({emitEvent: false}); + if (type === FormPropertyType.select) { + this.propertyFormGroup.get('multiple').enable({emitEvent: false}); + const multiple: boolean = this.propertyFormGroup.get('multiple').value; + if (multiple) { + this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false}); + } else { + this.propertyFormGroup.get('allowEmptyOption').enable({emitEvent: false}); + } + } } else { this.propertyFormGroup.get('multiple').disable({emitEvent: false}); + this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false}); this.propertyFormGroup.get('items').disable({emitEvent: false}); } + if (type === FormPropertyType.datetime) { + this.propertyFormGroup.get('allowClear').enable({emitEvent: false}); + this.propertyFormGroup.get('dateTimeType').enable({emitEvent: false}); + } else { + this.propertyFormGroup.get('allowClear').disable({emitEvent: false}); + this.propertyFormGroup.get('dateTimeType').disable({emitEvent: false}); + } + if (type === FormPropertyType.htmlSection) { + this.propertyFormGroup.get('htmlContent').enable({emitEvent: false}); + this.propertyFormGroup.get('htmlClassList').enable({emitEvent: false}); + } else { + this.propertyFormGroup.get('htmlContent').disable({emitEvent: false}); + this.propertyFormGroup.get('htmlClassList').disable({emitEvent: false}); + } + if ([FormPropertyType.javascript, FormPropertyType.markdown].includes(type)) { + this.propertyFormGroup.get('helpId').enable({emitEvent: false}); + } else { + this.propertyFormGroup.get('helpId').disable({emitEvent: false}); + } if (this.propertyType !== type) { const defaultValue = defaultPropertyValue(type); this.propertyFormGroup.get('default').patchValue(defaultValue, {emitEvent: false}); this.propertyType = type; + if (type === FormPropertyType.textarea) { + if (isUndefinedOrNull(this.propertyFormGroup.get('rows').value)) { + this.propertyFormGroup.get('rows').patchValue(2, {emitEvent: false}); + } + } + if (type === FormPropertyType.radios) { + if (isUndefinedOrNull(this.propertyFormGroup.get('direction').value)) { + this.propertyFormGroup.get('direction').patchValue('column', {emitEvent: false}); + } + } + if (type === FormPropertyType.datetime) { + if (isUndefinedOrNull(this.propertyFormGroup.get('dateTimeType').value)) { + this.propertyFormGroup.get('dateTimeType').patchValue('datetime', {emitEvent: false}); + } + if (isUndefinedOrNull(this.propertyFormGroup.get('allowClear').value)) { + this.propertyFormGroup.get('allowClear').patchValue(true, {emitEvent: false}); + } + } } } private onSelectItemsChange() { - const type: FormPropertyType = this.propertyFormGroup.get('type').value; + const type = this.propertyItemType; const multiple: boolean = this.propertyFormGroup.get('multiple').value; const defaultValue: any = this.propertyFormGroup.get('default').value; const items: FormSelectItem[] = this.propertyFormGroup.get('items').value; - if (defaultValue && type === FormPropertyType.select) { - if (multiple) { + if (defaultValue && [FormPropertyType.select, FormPropertyType.radios].includes(type)) { + if (multiple && FormPropertyType.select === type) { let targetValue: any[] = defaultValue; targetValue = targetValue.filter(valItem => !!items.find(item => item.value === valItem)); this.propertyFormGroup.get('default').patchValue(targetValue, {emitEvent: false}); @@ -190,7 +280,7 @@ export class DynamicFormPropertyPanelComponent implements OnInit { } private onMultipleSelectChange() { - const type: FormPropertyType = this.propertyFormGroup.get('type').value; + const type = this.propertyItemType; const multiple: boolean = this.propertyFormGroup.get('multiple').value; const defaultValue: any = this.propertyFormGroup.get('default').value; if (type === FormPropertyType.select) { @@ -199,6 +289,8 @@ export class DynamicFormPropertyPanelComponent implements OnInit { const newVal = [defaultValue]; this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false}); } + this.propertyFormGroup.get('allowEmptyOption').patchValue(false, {emitEvent: false}); + this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false}); } else { if (defaultValue && Array.isArray(defaultValue)) { const newVal = defaultValue.length ? defaultValue[0] : null; @@ -206,6 +298,7 @@ export class DynamicFormPropertyPanelComponent implements OnInit { this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false}); }); } + this.propertyFormGroup.get('allowEmptyOption').enable({emitEvent: false}); } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts index c734c91fab..2d18684227 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts @@ -42,12 +42,12 @@ import { 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 { + defaultPropertyValue, FormProperty, FormPropertyType, formPropertyTypes, - formPropertyTypeTranslations + formPropertyTypeTranslations, propertyValid } from '@shared/models/dynamic-form.models'; import { DynamicFormPropertiesComponent @@ -57,39 +57,6 @@ import { } 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: FormProperty): boolean => !(!property.id || !property.name || !property.type); - -export const defaultPropertyValue = (type: FormPropertyType): any => { - switch (type) { - case FormPropertyType.text: - return ''; - case FormPropertyType.number: - return 0; - case FormPropertyType.switch: - return false; - case FormPropertyType.color: - return '#000'; - case FormPropertyType.color_settings: - return constantColor('#000'); - case FormPropertyType.font: - return { - size: 12, - sizeUnit: 'px', - family: 'Roboto', - weight: 'normal', - style: 'normal', - lineHeight: '1' - } as Font; - case FormPropertyType.units: - return ''; - case FormPropertyType.icon: - return 'star'; - case FormPropertyType.fieldset: - case FormPropertyType.select: - return null; - } -}; - @Component({ selector: 'tb-dynamic-form-property-row', templateUrl: './dynamic-form-property-row.component.html', @@ -168,7 +135,7 @@ export class DynamicFormPropertyRowComponent implements ControlValueAccessor, On this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } setDisabledState(isDisabled: boolean): void { @@ -242,7 +209,7 @@ export class DynamicFormPropertyRowComponent implements ControlValueAccessor, On this.editProperty(null, this.editButton, true, onCanceled); } - public validate(c: UntypedFormControl) { + public validate(_c: UntypedFormControl) { const idControl = this.propertyRowFormGroup.get('id'); if (idControl.hasError('propertyIdNotUnique')) { idControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); 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 index 3e43c7a3cc..9db318f475 100644 --- 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 @@ -40,7 +40,6 @@ 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'; @@ -105,7 +104,7 @@ export class DynamicFormSelectItemRowComponent implements ControlValueAccessor, this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } setDisabledState(isDisabled: boolean): void { @@ -128,7 +127,7 @@ export class DynamicFormSelectItemRowComponent implements ControlValueAccessor, this.cd.markForCheck(); } - public validate(c: UntypedFormControl) { + public validate(_c: UntypedFormControl) { const valueControl = this.selectItemRowFormGroup.get('value'); if (valueControl.hasError('itemValueNotUnique')) { valueControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); 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 index 03e1fdcbf5..64918ab339 100644 --- 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 @@ -27,6 +27,7 @@ [cdkDropListDisabled]="!dragEnabled" (cdkDropListDropped)="selectItemDrop($event)">
) { const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray; const item = itemsArray.at(event.previousIndex); - itemsArray.removeAt(event.previousIndex); - itemsArray.insert(event.currentIndex, item); + itemsArray.removeAt(event.previousIndex, {emitEvent: false}); + itemsArray.insert(event.currentIndex, item, {emitEvent: true}); } selectItemsFormArray(): UntypedFormArray { return this.selectItemsFormGroup.get('selectItems') as UntypedFormArray; } - trackBySelectItem(index: number, selectItemControl: AbstractControl): any { + trackBySelectItem(_index: number, selectItemControl: AbstractControl): any { return selectItemControl; } 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 index 1acdcb6962..c617ef23d5 100644 --- 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 @@ -15,7 +15,8 @@ limitations under the License. --> -
{{ title }}
@@ -42,19 +43,50 @@ - - + + + + + + + + + + + + +
+ + +
+ + +
+ +
- -
+ +
+ + +
+
+
{{ propertyRow.label | customTranslate }}
-
+
{{ property.subLabel | customTranslate }}
- - -
{{ property.fieldSuffix | customTranslate }}
-
- - - - {{ item.label | customTranslate }} - - - + + + + - - -
{{ property.fieldSuffix | customTranslate }}
-
@@ -115,3 +135,132 @@
+ + + + + {{ property.name | customTranslate }} + + + + {{ property.name | customTranslate }} + +
{{ property.fieldSuffix | customTranslate }}
+
+ + {{ property.name | customTranslate }} + +
{{ property.fieldSuffix | customTranslate }}
+
+ + {{ property.name | customTranslate }} + + + + {{ item.label | customTranslate }} + + + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
{{ property.name | customTranslate }}
+ + +
{{ item.label | 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 index ed4f7d837c..90354e904e 100644 --- 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 @@ -13,10 +13,31 @@ * 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; } + .tb-form-row.tb-radios-property { + &.direction-column { + flex-direction: column; + align-items: flex-start; + gap: 4px; + } + .mat-mdc-radio-group { + overflow: hidden; + .mat-mdc-radio-button { + overflow: hidden; + .mdc-form-field { + overflow: hidden; + width: 100%; + .mdc-label { + overflow: hidden; + } + } + } + } + } } 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 index ac4348df16..3c9d7a1f83 100644 --- 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 @@ -44,11 +44,15 @@ import { FormPropertyContainerType, FormPropertyGroup, FormPropertyType, + isInputFieldPropertyType, PropertyConditionFunction, toPropertyGroups } from '@shared/models/dynamic-form.models'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import { ContentType } from '@shared/models/constants'; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-dynamic-form', @@ -69,10 +73,14 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; }) export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAccessor, Validator { + isInputFieldPropertyType = isInputFieldPropertyType; + FormPropertyContainerType = FormPropertyContainerType; FormPropertyType = FormPropertyType; + ContentType = ContentType; + @Input() disabled: boolean; @@ -82,10 +90,22 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce @Input() title: string; + @Input() + @coerceBoolean() + isArrayItem = false; + @Input() @coerceBoolean() stroked = false; + @Input() + @coerceBoolean() + noPadding = false; + + @Input() + @coerceBoolean() + noBorder = false; + private modelValue: {[id: string]: any}; private propagateChange = null; @@ -97,6 +117,8 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce propertyGroups: FormPropertyGroup[]; constructor(protected store: Store, + private customTranslate: CustomTranslatePipe, + private sanitizer: DomSanitizer, private destroyRef: DestroyRef, private fb: UntypedFormBuilder, private cd: ChangeDetectorRef) { @@ -173,26 +195,28 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce } } } - this.propertyGroups = toPropertyGroups(this.properties); + this.propertyGroups = toPropertyGroups(this.properties, this.isArrayItem, this.customTranslate, this.sanitizer); for (const property of this.properties) { - if (property.disableOnProperty) { - if (!this.validatorTriggers.includes(property.disableOnProperty)) { - this.validatorTriggers.push(property.disableOnProperty); + if (property.type !== FormPropertyType.htmlSection) { + 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)); + const validators: ValidatorFn[] = []; + if (property.required) { + validators.push(Validators.required); } - if (isDefinedAndNotNull(property.max)) { - validators.push(Validators.max(property.max)); + 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.propertiesFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false}); } } this.setupValue(); @@ -216,7 +240,7 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce } this.propertyGroups.forEach(g => { g.containers.forEach(container => { - if (container.type === FormPropertyContainerType.fieldset) { + if ([FormPropertyContainerType.fieldset, FormPropertyContainerType.field, FormPropertyContainerType.htmlSection, FormPropertyContainerType.array].includes(container.type)) { container.visible = container.property.visible; } else { container.visible = container.switch?.visible || container.properties.some(p => p.visible); @@ -233,12 +257,14 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce 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}); + if (property.type !== FormPropertyType.htmlSection) { + 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}); + } } } } 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 1fc32f8c67..627c0b6362 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 @@ -180,6 +180,9 @@ import { import { DynamicFormSelectItemRowComponent } from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component'; +import { + DynamicFormArrayComponent +} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component'; @NgModule({ declarations: [ @@ -248,7 +251,8 @@ import { DynamicFormPropertyPanelComponent, DynamicFormSelectItemsComponent, DynamicFormSelectItemRowComponent, - DynamicFormComponent + DynamicFormComponent, + DynamicFormArrayComponent ], imports: [ CommonModule, @@ -321,7 +325,8 @@ import { DynamicFormPropertyPanelComponent, DynamicFormSelectItemsComponent, DynamicFormSelectItemRowComponent, - DynamicFormComponent + DynamicFormComponent, + DynamicFormArrayComponent ], providers: [ ColorSettingsComponentService, 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 6e837b0740..648491faed 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 @@ -112,6 +112,7 @@ export class WidgetComponentService { templateHtml: this.utils.editWidgetInfo.templateHtml, templateCss: this.utils.editWidgetInfo.templateCss, controllerScript: this.utils.editWidgetInfo.controllerScript, + settingsForm: this.utils.editWidgetInfo.settingsForm, settingsSchema: this.utils.editWidgetInfo.settingsSchema, dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema, latestDataKeySettingsSchema: this.utils.editWidgetInfo.latestDataKeySettingsSchema, @@ -298,6 +299,9 @@ export class WidgetComponentService { this.loadWidgetResources(widgetInfo, widgetNamespace, [SharedModule, WidgetComponentsModule, this.homeComponentsModule]).subscribe( { next: () => { + if (widgetControllerDescriptor.settingsForm) { + widgetInfo.typeSettingsForm = widgetControllerDescriptor.settingsForm; + } if (widgetControllerDescriptor.settingsSchema) { widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema; } @@ -539,6 +543,9 @@ export class WidgetComponentService { const result: WidgetControllerDescriptor = { widgetTypeFunction: widgetType }; + if (isFunction(widgetTypeInstance.getSettingsForm)) { + result.settingsForm = widgetTypeInstance.getSettingsForm(); + } if (isFunction(widgetTypeInstance.getSettingsSchema)) { result.settingsSchema = widgetTypeInstance.getSettingsSchema(); } 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 31583747bc..8de7dbfb8b 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 @@ -561,6 +561,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri fullFqn: string; deprecated: boolean; scada: boolean; + typeSettingsForm?: FormProperty[]; typeSettingsSchema?: string | any; typeDataKeySettingsSchema?: string | any; typeLatestDataKeySettingsSchema?: string | any; @@ -644,6 +645,7 @@ export const ErrorWidgetType: WidgetInfo = { }; export interface WidgetTypeInstance { + getSettingsForm?: () => FormProperty[]; getSettingsSchema?: () => string; getDataKeySettingsSchema?: () => string; getLatestDataKeySettingsSchema?: () => string; @@ -672,6 +674,7 @@ export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({ templateHtml: widgetTypeEntity.descriptor.templateHtml, templateCss: widgetTypeEntity.descriptor.templateCss, controllerScript: widgetTypeEntity.descriptor.controllerScript, + settingsForm: widgetTypeEntity.descriptor.settingsForm, settingsSchema: widgetTypeEntity.descriptor.settingsSchema, dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema, latestDataKeySettingsSchema: widgetTypeEntity.descriptor.latestDataKeySettingsSchema, @@ -701,6 +704,7 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: templateHtml: widgetInfo.templateHtml, templateCss: widgetInfo.templateCss, controllerScript: widgetInfo.controllerScript, + settingsForm: widgetInfo.settingsForm, settingsSchema: widgetInfo.settingsSchema, dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema, latestDataKeySettingsSchema: widgetInfo.latestDataKeySettingsSchema, diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html index c0ee9b61b7..4cc1f0fedc 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html @@ -148,17 +148,15 @@
- -
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html index d566336057..fc02871a9c 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html @@ -28,6 +28,7 @@ [cdkDropListDisabled]="!dragEnabled" (cdkDropListDropped)="behaviorDrop($event)">
) { const behaviorsArray = this.behaviorsFormGroup.get('behaviors') as UntypedFormArray; const behavior = behaviorsArray.at(event.previousIndex); - behaviorsArray.removeAt(event.previousIndex); - behaviorsArray.insert(event.currentIndex, behavior); + behaviorsArray.removeAt(event.previousIndex, {emitEvent: false}); + behaviorsArray.insert(event.currentIndex, behavior, {emitEvent: true}); } behaviorsFormArray(): UntypedFormArray { diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 647ab6113d..8e500bdbe9 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -80,6 +80,8 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, toastTargetId = `jsFuncEditor-${guid()}`; + @Input() label: string; + @Input() functionTitle: string; @Input() functionName: string; @@ -102,7 +104,9 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, @Input() globalVariables: Array; - @Input() disableUndefinedCheck = false; + @Input() + @coerceBoolean() + disableUndefinedCheck = false; @Input() helpId: string; @@ -174,7 +178,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, } ngOnInit(): void { - if (this.functionTitle) { + if (this.functionTitle || this.label) { this.hideBrackets = true; } if (!this.resultType || this.resultType.length === 0) { @@ -190,6 +194,8 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, } if (this.functionTitle) { this.functionLabel = `${this.functionTitle}: f(${this.functionArgsString})`; + } else if (this.label) { + this.functionLabel = this.label; } else { this.functionLabel = `function ${this.functionName ? this.functionName : ''}(${this.functionArgsString})${this.hideBrackets ? '' : ' {'}`; diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index 375f22e02f..eb4c15fc18 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -15,7 +15,8 @@ limitations under the License. --> - diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index 951abaa12b..4121a7e8d7 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -108,6 +108,9 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { @Input() subscriptSizing: SubscriptSizing = 'fixed'; + @Input() + fieldClass: string; + @Input() @coerceArray() predefinedValues: StringItemsOption[]; diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.html b/ui-ngx/src/app/shared/components/time/datetime.component.html index edf59702f1..f0965deaf8 100644 --- a/ui-ngx/src/app/shared/components/time/datetime.component.html +++ b/ui-ngx/src/app/shared/components/time/datetime.component.html @@ -15,26 +15,26 @@ limitations under the License. --> -
- - {{ dateText | translate }} - - - - - - {{ timeText | translate }} - - - - -
+ + {{ dateText }} +
+ + +
+ + +
diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.scss b/ui-ngx/src/app/shared/components/time/datetime.component.scss new file mode 100644 index 0000000000..b3333d34d5 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/datetime.component.scss @@ -0,0 +1,60 @@ +/** + * 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 '../../../../scss/constants'; + +:host-context(.tb-form-row) { + :host { + display: flex; + } + .mat-mdc-form-field { + &.tb-datetime-field { + width: 185px; + &.with-clear { + width: 225px; + } + } + &.tb-date-field { + width: 140px; + &.with-clear { + width: 180px; + } + } + &.tb-time-field { + width: 100px; + &.with-clear { + width: 140px; + } + } + } +} + +:host-context(.tb-form-row.column-xs) { + @media #{$mat-xs} { + .mat-mdc-form-field { + &.tb-datetime-field, &.tb-date-field, &.tb-time-field { + width: auto; + flex: 1; + } + } + } +} + +:host { + .mat-mdc-form-field-icon-suffix, + .mat-datetimepicker-toggle { + color: rgba(0, 0, 0, 0.38); + } +} diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.ts b/ui-ngx/src/app/shared/components/time/datetime.component.ts index c004ab3786..52029fb958 100644 --- a/ui-ngx/src/app/shared/components/time/datetime.component.ts +++ b/ui-ngx/src/app/shared/components/time/datetime.component.ts @@ -17,11 +17,14 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; +import { MatDatetimepickerType } from '@mat-datetimepicker/core/datetimepicker/datetimepicker-type'; +import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-datetime', templateUrl: './datetime.component.html', - styleUrls: [], + styleUrls: ['./datetime.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, @@ -41,6 +44,18 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor { this.requiredValue = coerceBooleanProperty(value); } + @Input() + floatLabel: FloatLabelType = 'auto'; + + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @Input() + appearance: MatFormFieldAppearance = 'fill'; + + @Input() + type: MatDatetimepickerType = 'datetime'; + @Input() disabled: boolean; @@ -48,10 +63,15 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor { dateText: string; @Input() - timeText: string; + @coerceBoolean() + showLabel = true; @Input() - showLabel = true; + @coerceBoolean() + allowClear = false; + + @Input() + fieldClass: string; minDateValue: Date | null; @@ -111,4 +131,10 @@ export class DatetimeComponent implements OnInit, ControlValueAccessor { this.updateView(value); } + clear($event: Event) { + $event.stopPropagation(); + this.date = null; + this.updateView(null); + } + } diff --git a/ui-ngx/src/app/shared/models/dynamic-form.models.ts b/ui-ngx/src/app/shared/models/dynamic-form.models.ts index 70c70b42b5..773f6bdebe 100644 --- a/ui-ngx/src/app/shared/models/dynamic-form.models.ts +++ b/ui-ngx/src/app/shared/models/dynamic-form.models.ts @@ -16,22 +16,36 @@ import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models'; -import { isString } from '@core/utils'; +import { deepClone, isDefinedAndNotNull, 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'; +import { constantColor, Font } from '@shared/models/widget-settings.models'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; export enum FormPropertyType { text = 'text', number = 'number', + password = 'password', + textarea = 'textarea', switch = 'switch', select = 'select', + radios = 'radios', + datetime = 'datetime', + image = 'image', + javascript = 'javascript', + json = 'json', + html = 'html', + css = 'css', + markdown = 'markdown', color = 'color', color_settings = 'color_settings', font = 'font', units = 'units', icon = 'icon', - fieldset = 'fieldset' + fieldset = 'fieldset', + array = 'array', + htmlSection = 'htmlSection' } export const formPropertyTypes = Object.keys(FormPropertyType) as FormPropertyType[]; @@ -40,14 +54,26 @@ export const formPropertyTypeTranslations = new Map( [ [FormPropertyType.text, 'dynamic-form.property.type-text'], [FormPropertyType.number, 'dynamic-form.property.type-number'], + [FormPropertyType.password, 'dynamic-form.property.type-password'], + [FormPropertyType.textarea, 'dynamic-form.property.type-textarea'], [FormPropertyType.switch, 'dynamic-form.property.type-switch'], [FormPropertyType.select, 'dynamic-form.property.type-select'], + [FormPropertyType.radios, 'dynamic-form.property.type-radios'], + [FormPropertyType.datetime, 'dynamic-form.property.type-datetime'], + [FormPropertyType.image, 'dynamic-form.property.type-image'], + [FormPropertyType.javascript, 'dynamic-form.property.type-javascript'], + [FormPropertyType.json, 'dynamic-form.property.type-json'], + [FormPropertyType.html, 'dynamic-form.property.type-html'], + [FormPropertyType.css, 'dynamic-form.property.type-css'], + [FormPropertyType.markdown, 'dynamic-form.property.type-markdown'], [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'] + [FormPropertyType.fieldset, 'dynamic-form.property.type-fieldset'], + [FormPropertyType.array, 'dynamic-form.property.type-array'], + [FormPropertyType.htmlSection, 'dynamic-form.property.type-html-section'] ] ); @@ -78,6 +104,10 @@ export interface FormPropertyBase { fieldClass?: string; } +export interface FormTextareaProperty extends FormPropertyBase { + rows?: number; +} + export interface FormNumberProperty extends FormPropertyBase { min?: number; max?: number; @@ -88,6 +118,11 @@ export interface FormFieldSetProperty extends FormPropertyBase { properties?: FormProperty[]; } +export interface FormArrayProperty extends FormPropertyBase { + arrayItemName?: string; + arrayItemType?: FormPropertyType; +} + export interface FormSelectItem { value: any; label: string; @@ -95,33 +130,82 @@ export interface FormSelectItem { export interface FormSelectProperty extends FormPropertyBase { multiple?: boolean; + allowEmptyOption?: boolean; items?: FormSelectItem[]; } -export type FormProperty = FormPropertyBase & FormNumberProperty & FormSelectProperty & FormFieldSetProperty; +export type FormPropertyDirection = 'row' | 'column'; + +export interface FormRadiosProperty extends FormPropertyBase { + direction?: FormPropertyDirection; + items?: FormSelectItem[]; +} + +export type FormPropertyDateTimeType = 'date' | 'time' | 'datetime'; + +export interface FormDateTimeProperty extends FormPropertyBase { + allowClear?: boolean; + dateTimeType?: FormPropertyDateTimeType; +} + +export interface FormJavascriptProperty extends FormPropertyBase { + helpId?: string; +} + +export interface FormMarkdownProperty extends FormPropertyBase { + helpId?: string; +} + +export interface FormHtmlSection extends FormPropertyBase { + htmlClassList?: string[]; + htmlContent?: string; +} + +export type FormProperty = FormPropertyBase & FormTextareaProperty & FormNumberProperty & FormSelectProperty & FormRadiosProperty + & FormDateTimeProperty & FormJavascriptProperty & FormMarkdownProperty & FormFieldSetProperty & FormArrayProperty & FormHtmlSection; export enum FormPropertyContainerType { + field = 'field', row = 'row', - fieldset = 'fieldset' + fieldset = 'fieldset', + array = 'array', + htmlSection = 'htmlSection' } export interface FormPropertyContainerBase { type: FormPropertyContainerType; label: string; - properties: FormProperty[]; visible: boolean; } export interface FormPropertyRow extends FormPropertyContainerBase { + properties?: FormProperty[]; switch?: FormProperty; rowClass?: string; + propertiesRowClass?: string; +} + +export interface FormPropertyField extends FormPropertyContainerBase { + property?: FormProperty; } export interface FormPropertyFieldset extends FormPropertyContainerBase { property?: FormProperty; + properties?: FormProperty[]; } -export type FormPropertyContainer = FormPropertyRow & FormPropertyFieldset; +export interface FormPropertyArray extends FormPropertyContainerBase { + property?: FormProperty; + arrayItemProperty?: FormProperty; +} + +export interface FormPropertyHtml extends FormPropertyContainerBase { + property?: FormProperty; + safeHtml?: SafeHtml; + htmlClass?: string; +} + +export type FormPropertyContainer = FormPropertyField & FormPropertyRow & FormPropertyFieldset & FormPropertyHtml; export interface FormPropertyGroup { title?: string; @@ -129,14 +213,22 @@ export interface FormPropertyGroup { visible: boolean; } -export const toPropertyGroups = (properties: FormProperty[]): FormPropertyGroup[] => { +export const toPropertyGroups = (properties: FormProperty[], + isArrayItem: boolean, + customTranslate: CustomTranslatePipe, + sanitizer: DomSanitizer): FormPropertyGroup[] => { const groups: {title: string, properties: FormProperty[]}[] = []; for (let property of properties) { if (!property.group) { - groups.push({ - title: null, - properties: [property] - }); + let group = groups.length ? groups[groups.length - 1] : null; + if (group && !group.title) { + group.properties.push(property); + } else { + groups.push({ + title: null, + properties: [property] + }); + } } else { let propertyGroup = groups.find(g => g.title === property.group); if (!propertyGroup) { @@ -151,15 +243,35 @@ export const toPropertyGroups = (properties: FormProperty[]): FormPropertyGroup[ } return groups.map(g => ({ title: g.title, - containers: toPropertyContainers(g.properties), + containers: toPropertyContainers(g.properties, isArrayItem, customTranslate, sanitizer), visible: true })); }; -const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer[] => { +const toPropertyContainers = (properties: FormProperty[], + isArrayItem: boolean, + customTranslate: CustomTranslatePipe, + sanitizer: DomSanitizer): FormPropertyContainer[] => { const result: FormPropertyContainer[] = []; for (let property of properties) { - if (property.type === FormPropertyType.fieldset) { + if (property.type === FormPropertyType.array) { + const propertyArray: FormPropertyArray = { + property, + label: property.name, + type: FormPropertyContainerType.array, + visible: true + }; + const arrayItemProperty = deepClone(property); + arrayItemProperty.name = property.arrayItemName; + arrayItemProperty.type = property.arrayItemType; + arrayItemProperty.required = true; + delete arrayItemProperty.disableOnProperty; + delete arrayItemProperty.condition; + delete arrayItemProperty.conditionFunction; + delete arrayItemProperty.group; + propertyArray.arrayItemProperty = arrayItemProperty; + result.push(propertyArray); + } else if (property.type === FormPropertyType.fieldset) { const propertyFieldset: FormPropertyFieldset = { property, label: property.name, @@ -168,6 +280,24 @@ const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer visible: true }; result.push(propertyFieldset); + } else if (property.type === FormPropertyType.htmlSection) { + const propertyHtml: FormPropertyHtml = { + property, + label: property.name, + type: FormPropertyContainerType.htmlSection, + htmlClass: property.htmlClassList ? property.htmlClassList.join(' ') : '', + safeHtml: sanitizer.bypassSecurityTrustHtml(property.htmlContent), + visible: true + }; + result.push(propertyHtml); + } else if (isSingleFieldPropertyType(property.type) || isArrayItem) { + const propertyField: FormPropertyField = { + property, + label: property.name, + type: FormPropertyContainerType.field, + visible: true + }; + result.push(propertyField); } else { let propertyRow = result.find(r => r.type === FormPropertyContainerType.row && r.label === property.name); @@ -177,6 +307,7 @@ const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer type: FormPropertyContainerType.row, properties: [], rowClass: property.rowClass, + propertiesRowClass: 'row flex-end align-center', visible: true }; result.push(propertyRow); @@ -188,22 +319,60 @@ const toPropertyContainers = (properties: FormProperty[]): FormPropertyContainer } } } + for (let container of result.filter(c => + c.type === FormPropertyContainerType.row && !c.switch && c.properties?.length === 1)) { + const property = container.properties[0]; + if (isInputFieldPropertyType(property.type)) { + const labelText = customTranslate.transform(property.name); + if (property.type !== FormPropertyType.number && labelText.length > 40) { + container.type = FormPropertyContainerType.field; + container.property = property; + delete container.properties; + delete container.rowClass; + } else { + if (!container.rowClass) { + container.rowClass = 'column-xs'; + container.propertiesRowClass = 'gt-xs:align-center xs:flex-col gt-xs:flex-row gt-xs:justify-end'; + } + } + } + } return result; } +export const isPropertyTypeAllowedForRow = (type: FormPropertyType): boolean => { + return !isSingleFieldPropertyType(type) && ![FormPropertyType.fieldset, FormPropertyType.array, FormPropertyType.htmlSection].includes(type); +} + +export const isSingleFieldPropertyType = (type: FormPropertyType): boolean => { + return [FormPropertyType.radios, FormPropertyType.textarea, FormPropertyType.image, FormPropertyType.javascript, FormPropertyType.json, FormPropertyType.html, + FormPropertyType.css, FormPropertyType.markdown].includes(type); +} + +export const isInputFieldPropertyType = (type: FormPropertyType): boolean => { + return [FormPropertyType.text, FormPropertyType.password, FormPropertyType.number, FormPropertyType.select, FormPropertyType.datetime, + FormPropertyType.textarea].includes(type); +} + export const defaultFormProperties = (properties: FormProperty[]): {[id: string]: any} => { const formProperties: {[id: string]: any} = {}; for (const property of properties) { - formProperties[property.id] = defaultFormPropertyValue(property); + if (property.type !== FormPropertyType.htmlSection) { + formProperties[property.id] = defaultFormPropertyValue(property); + } } return formProperties; }; export const defaultFormPropertyValue = (property: FormProperty): any => { - if (property.type === FormPropertyType.fieldset) { + if (property.type === FormPropertyType.array) { + return []; + } else if (property.type === FormPropertyType.fieldset) { const propertyValue: {[id: string]: any} = {}; for (const childProperty of property.properties) { - propertyValue[childProperty.id] = defaultFormPropertyValue(childProperty); + if (childProperty.type !== FormPropertyType.htmlSection) { + propertyValue[childProperty.id] = defaultFormPropertyValue(childProperty); + } } return propertyValue; } else { @@ -211,10 +380,58 @@ export const defaultFormPropertyValue = (property: FormProperty): any => { } } +export const propertyValid = (property: FormProperty): boolean => + !(!property.id || !property.name || !property.type || (property.type === FormPropertyType.array && !property.arrayItemType)); + +export const defaultPropertyValue = (type: FormPropertyType): any => { + switch (type) { + case FormPropertyType.text: + case FormPropertyType.textarea: + case FormPropertyType.password: + case FormPropertyType.javascript: + case FormPropertyType.json: + case FormPropertyType.html: + case FormPropertyType.css: + case FormPropertyType.markdown: + return ''; + case FormPropertyType.number: + return 0; + case FormPropertyType.switch: + return false; + case FormPropertyType.color: + return '#000'; + case FormPropertyType.color_settings: + return constantColor('#000'); + case FormPropertyType.font: + return { + size: 12, + sizeUnit: 'px', + family: 'Roboto', + weight: 'normal', + style: 'normal', + lineHeight: '1' + } as Font; + case FormPropertyType.units: + return ''; + case FormPropertyType.icon: + return 'star'; + case FormPropertyType.fieldset: + case FormPropertyType.array: + case FormPropertyType.select: + case FormPropertyType.radios: + case FormPropertyType.datetime: + case FormPropertyType.htmlSection: + case FormPropertyType.image: + return null; + } +}; + export const formPropertyCompletions = (properties: FormProperty[], customTranslate: CustomTranslatePipe): TbEditorCompletions => { const propertiesCompletions: TbEditorCompletions = {}; for (const property of properties) { - propertiesCompletions[property.id] = formPropertyCompletion(property, customTranslate); + if (property.type !== FormPropertyType.htmlSection) { + propertiesCompletions[property.id] = formPropertyCompletion(property, customTranslate); + } } return propertiesCompletions; } @@ -224,37 +441,63 @@ export const formPropertyCompletion = (property: FormProperty, customTranslate: if (property.subLabel) { description += ` ${customTranslate.transform(property.subLabel, property.subLabel)}`; } - if (property.type === FormPropertyType.select) { - if (property.multiple) { + const isArray = property.type === FormPropertyType.array; + const type = isArray ? property.arrayItemType : property.type; + if (type === FormPropertyType.select) { + if (property.multiple || isArray) { description += '

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

Possible values:'; } description += `
    ${property.items.map(item => `
  • ${item.value} ${typeof item.value}
  • `).join('\n')}
`; } + if (type === FormPropertyType.datetime) { + if (isArray) { + description += '

Stores array of time values in milliseconds since midnight, January 1, 1970 UTC.'; + } else { + description += '

Stores time value in milliseconds since midnight, January 1, 1970 UTC.'; + } + } const completion: TbEditorCompletion = { meta: 'property', description, type: formPropertyCompletionType(property) }; - if (property.type === FormPropertyType.fieldset) { + if (type === FormPropertyType.fieldset && !isArray) { completion.children = {}; for (const childProperty of property.properties) { - completion.children[childProperty.id] = formPropertyCompletion(childProperty, customTranslate); + if (childProperty.type !== FormPropertyType.htmlSection) { + completion.children[childProperty.id] = formPropertyCompletion(childProperty, customTranslate); + } } } return completion; }; const formPropertyCompletionType = (property: FormProperty): string => { - switch (property.type) { + const isArray = property.type === FormPropertyType.array; + const type = isArray ? property.arrayItemType : property.type; + let typeStr: string; + switch (type) { case FormPropertyType.text: - return 'string'; + case FormPropertyType.password: + case FormPropertyType.textarea: + typeStr = 'string'; + break; case FormPropertyType.number: - return 'number'; + typeStr = 'number'; + break; case FormPropertyType.switch: - return 'boolean'; + typeStr = 'boolean'; + break; + case FormPropertyType.datetime: + typeStr = 'number'; + break; + case FormPropertyType.image: + typeStr = 'image URL string'; + break; case FormPropertyType.select: + case FormPropertyType.radios: const items = property.items || []; const types: string[] = []; items.forEach(item => { @@ -264,24 +507,53 @@ const formPropertyCompletionType = (property: FormProperty): string => { } }); const typesString = types.length ? types.join(' | ') : 'string'; - if (property.multiple) { - return `Array<${typesString}>`; + if (property.type === FormPropertyType.select && property.multiple) { + typeStr = `Array<${typesString}>`; } else { - return typesString; + typeStr = typesString; } + break; case FormPropertyType.color: - return 'color string'; + typeStr = 'color string'; + break; case FormPropertyType.color_settings: - return 'ColorProcessor'; + typeStr = 'ColorProcessor'; + break; case FormPropertyType.font: - return 'Font'; + typeStr = 'Font'; + break; case FormPropertyType.units: - return 'units string'; + typeStr = 'units string'; + break; case FormPropertyType.icon: - return 'icon string'; + typeStr = 'icon string'; + break; case FormPropertyType.fieldset: - return 'object'; + typeStr = 'object'; + break; + case FormPropertyType.javascript: + typeStr = 'JavaScript function body string'; + break; + case FormPropertyType.json: + typeStr = 'JSON string'; + break; + case FormPropertyType.html: + typeStr = 'HTML string'; + break; + case FormPropertyType.css: + typeStr = 'CSS string'; + break; + case FormPropertyType.markdown: + typeStr = 'Markdown string'; + break; + default: + typeStr = 'unknown'; + break; + } + if (isArray) { + typeStr = `Array<${typeStr}>`; } + return typeStr; }; @@ -322,13 +594,14 @@ const schemaFormToProperties = (schema: JsonSchema, theForm: any[], groupTitle?: 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, + const id = form.key[level] + ''; + let property: FormProperty = { + id, + name: form.title || id, group: groupTitle, type: null, - default: form.schema.default, - required: form.required + default: form.schema?.default || null, + required: isDefinedAndNotNull(form.required) ? form.required : false }; if (form.condition?.length) { property.condition = `return ${form.condition};`; @@ -341,6 +614,14 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: property.type = FormPropertyType.text; property.fieldClass = 'flex'; break; + case 'password': + property.type = FormPropertyType.password; + property.fieldClass = 'flex'; + break; + case 'textarea': + property.type = FormPropertyType.textarea; + property.rows = form.rows || form.rowsMax || 2; + break; case 'checkbox': property.type = FormPropertyType.switch; break; @@ -353,6 +634,7 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: } property.multiple = form.multiple; property.fieldClass = 'flex'; + property.allowEmptyOption = isDefinedAndNotNull(form.allowClear) ? form.allowClear : false; break; case 'select': property.type = FormPropertyType.select; @@ -363,6 +645,25 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: } property.multiple = false; property.fieldClass = 'flex'; + property.allowEmptyOption = false; + break; + case 'radios': + property.type = FormPropertyType.radios; + if (form.titleMap?.length) { + property.items = form.titleMap.map(item => ({value: item.value, label: item.name})); + } else { + property.items = []; + } + property.direction = form.direction === 'row' ? 'row' : 'column'; + break; + case 'date': + property.type = FormPropertyType.datetime; + property.dateTimeType = 'date'; + property.fieldClass = 'flex'; + property.allowClear = true; + break; + case 'image': + property.type = FormPropertyType.image; break; case 'color': property.type = FormPropertyType.color; @@ -375,6 +676,44 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: property.properties = form.items ? (form.items as JsonFormData[]).map(item => jsonFormDataToProperty(item, level+1)).filter(p => p !== null) : []; break; + case 'javascript': + property.type = FormPropertyType.javascript; + property.helpId = form.helpId; + break; + case 'json': + property.type = FormPropertyType.json; + break; + case 'html': + property.type = FormPropertyType.html; + break; + case 'css': + property.type = FormPropertyType.css; + break; + case 'markdown': + property.type = FormPropertyType.markdown; + break; + case 'help': + property.type = FormPropertyType.htmlSection; + property.htmlContent = form.description || ''; + property.htmlClassList = form.htmlClass ? form.htmlClass.split(' ') : []; + break; + case 'array': + if (form.items?.length) { + const item: JsonFormData = form.items[0] as JsonFormData; + if (item.type !== 'array') { + const arrayProperty = jsonFormDataToProperty(item, 0); + arrayProperty.arrayItemType = arrayProperty.type; + arrayProperty.arrayItemName = arrayProperty.name; + arrayProperty.id = property.id; + arrayProperty.name = property.name; + arrayProperty.group = property.group; + arrayProperty.condition = property.condition; + arrayProperty.required = property.required; + property = arrayProperty; + property.type = FormPropertyType.array; + } + } + break; } if (!property.type) { 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 6f5579c77f..f3c82571e1 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -195,6 +195,7 @@ export interface WidgetTypeParameters { export interface WidgetControllerDescriptor { widgetTypeFunction?: any; + settingsForm?: FormProperty[]; settingsSchema?: string | any; dataKeySettingsSchema?: string | any; latestDataKeySettingsSchema?: string | any; 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 c43ff76da3..58e947e80e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1625,15 +1625,27 @@ "name": "Name", "type": "Type", "type-text": "Text", + "type-password": "Password", + "type-textarea": "Text area", "type-number": "Number", "type-switch": "Switch", "type-select": "Select", + "type-radios": "Radio buttons", + "type-datetime": "Date/Time", + "type-image": "Image", + "type-javascript": "JavaScript", + "type-json": "JSON", + "type-html": "HTML", + "type-css": "CSS", + "type-markdown": "Markdown", "type-color": "Color", "type-color-settings": "Color settings", "type-font": "Font", "type-units": "Units", "type-icon": "Icon", "type-fieldset": "Fieldset", + "type-array": "Array", + "type-html-section": "HTML section", "group-title": "Group title", "no-properties": "No properties configured", "add-property": "Add property", @@ -1655,13 +1667,32 @@ "property-field-classes": "Property field classes", "not-unique-property-ids-error": "Property Ids must be unique!", "enable-multiple-select": "Enable multiple select", + "allow-empty-select-option": "Allow empty option", "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" + "remove-option": "Remove option", + "textarea-rows": "Textarea rows", + "help-id": "Help id", + "buttons-direction": "Buttons direction", + "direction-row": "Row", + "direction-column": "Column", + "radio-button-options": "Radio button options", + "datetime-type": "Date/Time field type", + "datetime-type-date": "Date", + "datetime-type-time": "Time", + "datetime-type-datetime": "Date/Time", + "enable-clear-button": "Enable clear button", + "html-section-settings": "HTML section settings", + "html-section-classes": "HTML section classes", + "html-section-content": "HTML section content", + "array-item": "Array item", + "item-type": "Item type", + "item-name": "Item name", + "no-items": "No items" } }, "asset-profile": { @@ -2548,9 +2579,7 @@ "select-entity-view": "Select entity view", "make-public": "Make entity view public", "make-private": "Make entity view private", - "start-date": "Start date", "start-ts": "Start time", - "end-date": "End date", "end-ts": "End time", "date-limits": "Date limits", "client-attributes": "Client attributes", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 4a4d018f26..14a3455685 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -54,6 +54,16 @@ padding: 12px; gap: 8px; } + &.no-padding-right { + padding-right: 0; + .mat-expansion-panel { + &.tb-settings { + > .mat-expansion-panel-header { + padding-right: 16px; + } + } + } + } &.no-padding-bottom { padding-bottom: 0; }