diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts index 5c463b7165..c77065fbf8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts @@ -145,6 +145,12 @@ export const iotSvgPropertyTypeTranslations = new Mapscada.behavior.value-type - +
{{ valueTypesMap.get(behaviorFormGroup.get('valueType').value).name | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts index 1fba319cb2..12bc0d2ecc 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behavior-row.component.ts @@ -30,11 +30,11 @@ import { } from '@angular/core'; import { AbstractControl, - ControlValueAccessor, + ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, - UntypedFormBuilder, + UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - ValidationErrors, + ValidationErrors, Validator, ValidatorFn, Validators } from '@angular/forms'; import { @@ -50,6 +50,9 @@ import { ScadaSymbolBehaviorPanelComponent } from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component'; import { ValueToDataType } from '@shared/models/action-widget-settings.models'; +import { + ScadaSymbolBehaviorsComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component'; export const behaviorValid = (behavior: IotSvgBehavior): boolean => { if (!behavior.id || !behavior.name || !behavior.type) { @@ -80,16 +83,6 @@ export const behaviorValid = (behavior: IotSvgBehavior): boolean => { return true; }; -export const behaviorValidator = (control: AbstractControl): ValidationErrors | null => { - const behavior: IotSvgBehavior = control.value; - if (!behaviorValid(behavior)) { - return { - behavior: true - }; - } - return null; -}; - @Component({ selector: 'tb-scada-symbol-metadata-behavior-row', templateUrl: './scada-symbol-behavior-row.component.html', @@ -99,11 +92,16 @@ export const behaviorValidator = (control: AbstractControl): ValidationErrors | provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolBehaviorRowComponent), + multi: true } ], encapsulation: ViewEncapsulation.None }) -export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, OnInit { +export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, OnInit, Validator { @ViewChild('idInput') idInput: ElementRef; @@ -117,6 +115,9 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On @Input() disabled: boolean; + @Input() + index: number; + @Output() behaviorRemoved = new EventEmitter(); @@ -130,12 +131,13 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On private cd: ChangeDetectorRef, private popoverService: TbPopoverService, private renderer: Renderer2, - private viewContainerRef: ViewContainerRef) { + private viewContainerRef: ViewContainerRef, + private behaviorsComponent: ScadaSymbolBehaviorsComponent) { } ngOnInit() { this.behaviorRowFormGroup = this.fb.group({ - id: [null, [Validators.required]], + id: [null, [this.behaviorIdValidator()]], name: [null, [Validators.required]], type: [null, [Validators.required]] }); @@ -223,6 +225,42 @@ export class ScadaSymbolBehaviorRowComponent implements ControlValueAccessor, On this.editBehavior(null, this.editButton, true, onCanceled); } + public validate(c: UntypedFormControl) { + const idControl = this.behaviorRowFormGroup.get('id'); + if (idControl.hasError('behaviorIdNotUnique')) { + idControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); + } + if (idControl.hasError('behaviorIdNotUnique')) { + this.behaviorRowFormGroup.get('id').markAsTouched(); + return { + behaviorIdNotUnique: true + }; + } + const behavior: IotSvgBehavior = {...this.modelValue, ...this.behaviorRowFormGroup.value}; + if (!behaviorValid(behavior)) { + return { + behavior: true + }; + } + return null; + } + + private behaviorIdValidator(): ValidatorFn { + return control => { + if (!control.value) { + return { + required: true + }; + } + if (!this.behaviorsComponent.behaviorIdUnique(control.value, this.index)) { + return { + behaviorIdNotUnique: true + }; + } + return null; + }; + } + private onTypeChanged(newType: IotSvgBehaviorType) { const prevType = this.modelValue.type; this.modelValue = {...this.modelValue, ...{type: newType}}; 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 28efaca269..7d5991ed4b 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 @@ -31,6 +31,7 @@ class="tb-draggable-form-table-row" *ngFor="let behaviorControl of behaviorsFormArray().controls; trackBy: trackByBehavior; let $index = index;"> @@ -46,6 +47,7 @@
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts index 96f4b0b184..75e6fcc8bb 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts @@ -128,7 +128,6 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni } writeValue(value: IotSvgMetadata): void { - this.selectedOption = 'general'; this.modelValue = value; this.metadataFormGroup.patchValue( value, {emitEvent: false} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html new file mode 100644 index 0000000000..c0f3316bbe --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html @@ -0,0 +1,63 @@ + +
+
+
+
scada.property.id
+
scada.property.name
+
scada.property.type
+
+
+
+
+ + +
+ +
+
+
+ +
+
+ +
+
+ + + {{ 'scada.property.no-properties' | translate }} + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss new file mode 100644 index 0000000000..9bafbf2ccb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-scada-symbol-properties { + flex: 1; + margin: 12px; + .tb-form-table-header-cell { + &.tb-id-header { + flex: 1 1 40%; + } + &.tb-name-header { + flex: 1 1 40%; + } + &.tb-type-header { + flex: 1 1 20%; + } + &.tb-actions-header { + width: 120px; + min-width: 120px; + } + } + .tb-form-table { + overflow: hidden; + } + .tb-form-table-body { + overflow: auto; + tb-scada-symbol-metadata-property-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts new file mode 100644 index 0000000000..a36536464f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts @@ -0,0 +1,207 @@ +/// +/// 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, + forwardRef, + HostBinding, + Input, + OnInit, + QueryList, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator +} from '@angular/forms'; +import { IotSvgProperty, IotSvgPropertyType } from '@home/components/widget/lib/svg/iot-svg.models'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { + propertyValid, + ScadaSymbolPropertyRowComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'tb-scada-symbol-metadata-properties', + templateUrl: './scada-symbol-properties.component.html', + styleUrls: ['./scada-symbol-properties.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolPropertiesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolPropertiesComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnInit, Validator { + + @HostBinding('style.display') styleDisplay = 'flex'; + @HostBinding('style.overflow') styleOverflow = 'hidden'; + + @ViewChildren(ScadaSymbolPropertyRowComponent) + propertyRows: QueryList; + + @Input() + disabled: boolean; + + booleanPropertyIds: string[] = []; + + propertiesFormGroup: UntypedFormGroup; + + errorText = ''; + + get dragEnabled(): boolean { + return this.propertiesFormArray().controls.length > 1; + } + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private translate: TranslateService) { + } + + ngOnInit() { + this.propertiesFormGroup = this.fb.group({ + properties: this.fb.array([]) + }); + this.propertiesFormGroup.valueChanges.subscribe( + () => { + let properties: IotSvgProperty[] = this.propertiesFormGroup.get('properties').value; + if (properties) { + properties = properties.filter(p => propertyValid(p)); + } + this.booleanPropertyIds = properties.filter(p => p.type === IotSvgPropertyType.switch).map(p => p.id); + this.propagateChange(properties); + } + ); + } + + 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(value: IotSvgProperty[] | undefined): void { + const properties= value || []; + this.propertiesFormGroup.setControl('properties', this.preparePropertiesFormArray(properties), {emitEvent: false}); + this.booleanPropertyIds = properties.filter(p => p.type === IotSvgPropertyType.switch).map(p => p.id); + } + + public validate(c: UntypedFormControl) { + this.errorText = ''; + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + const notUniqueControls = + propertiesArray.controls.filter(control => control.hasError('propertyIdNotUnique')); + for (const control of notUniqueControls) { + control.updateValueAndValidity({onlySelf: false, emitEvent: false}); + if (control.hasError('propertyIdNotUnique')) { + this.errorText = this.translate.instant('scada.property.not-unique-property-ids-error'); + } + } + const valid = this.propertiesFormGroup.valid; + return valid ? null : { + properties: { + valid: false, + }, + }; + } + + public propertyIdUnique(id: string, index: number): boolean { + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + for (let i = 0; i < propertiesArray.controls.length; i++) { + if (i !== index) { + const otherControl = propertiesArray.controls[i]; + if (id === otherControl.value.id) { + return false; + } + } + } + return true; + } + + 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); + } + + 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: IotSvgProperty = { + id: '', + name: '', + type: IotSvgPropertyType.text, + default: '' + }; + const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray; + const propertyControl = this.fb.control(property, []); + propertiesArray.push(propertyControl); + setTimeout(() => { + const propertyRow = this.propertyRows.get(this.propertyRows.length-1); + propertyRow.onAdd(() => { + this.removeProperty(propertiesArray.length-1); + }); + }); + } + + private preparePropertiesFormArray(properties: IotSvgProperty[] | undefined): UntypedFormArray { + const propertiesControls: Array = []; + if (properties) { + properties.forEach((property) => { + propertiesControls.push(this.fb.control(property, [])); + }); + } + return this.fb.array(propertiesControls); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html new file mode 100644 index 0000000000..8a76578487 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html @@ -0,0 +1,184 @@ + +
+
{{ panelTitle | translate }}
+
+
+
scada.property.id
+ + + +
+
+
scada.property.name
+ + + +
+
+
scada.property.type
+ + + + {{ iotSvgPropertyTypeTranslations.get(type) | translate }} + + + +
+
+
scada.property.default-value
+ + + + + + + + + + + + + + +
+
+
scada.property.number-settings
+
+
scada.property.min
+ + + + +
scada.property.max
+ + + + +
scada.property.step
+ + + +
+
+
+ + {{ 'scada.property.value-required' | translate }} + +
+
+ + + + {{ 'scada.property.advanced-ui-settings' | translate }} + + + +
+
scada.property.sub-label
+ + + +
+
+ + {{ 'scada.property.vertical-divider-after' | translate }} + +
+
+
scada.property.input-field-suffix
+ + + +
+
+
scada.property.disable-on-property
+ + + + {{ prop }} + + + +
+
+
scada.property.property-row-classes
+ + + + {{ clazz }} + + + +
+
+
scada.property.property-field-classes
+ + + + {{ clazz }} + + + +
+
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss new file mode 100644 index 0000000000..c4b5b153d9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss @@ -0,0 +1,49 @@ +/** + * 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-scada-symbol-property-settings-panel { + width: 540px; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-lt-md} { + width: 90vw; + } + .tb-scada-symbol-property-settings-panel-content { + display: flex; + flex-direction: column; + gap: 16px; + overflow: auto; + margin: -10px; + padding: 10px; + } + .tb-scada-symbol-property-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-scada-symbol-property-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts new file mode 100644 index 0000000000..9be8959dca --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts @@ -0,0 +1,129 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { + IotSvgProperty, iotSvgPropertyFieldClasses, iotSvgPropertyRowClasses, + IotSvgPropertyType, + iotSvgPropertyTypes, + iotSvgPropertyTypeTranslations +} from '@home/components/widget/lib/svg/iot-svg.models'; +import { WidgetService } from '@core/http/widget.service'; +import { defaultPropertyValue } from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component'; +import { ValueType } from '@shared/models/constants'; + +@Component({ + selector: 'tb-scada-symbol-property-panel', + templateUrl: './scada-symbol-property-panel.component.html', + styleUrls: ['./scada-symbol-property-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolPropertyPanelComponent implements OnInit { + + ValueType = ValueType; + + IotSvgPropertyType = IotSvgPropertyType; + + iotSvgPropertyTypes = iotSvgPropertyTypes; + iotSvgPropertyTypeTranslations = iotSvgPropertyTypeTranslations; + + iotSvgPropertyRowClasses = iotSvgPropertyRowClasses; + + iotSvgPropertyFieldClasses = iotSvgPropertyFieldClasses; + + @Input() + isAdd = false; + + @Input() + property: IotSvgProperty; + + @Input() + booleanPropertyIds: string[]; + + @Input() + popover: TbPopoverComponent; + + @Output() + propertySettingsApplied = new EventEmitter(); + + panelTitle: string; + + propertyFormGroup: UntypedFormGroup; + + private propertyType: IotSvgPropertyType; + + constructor(private fb: UntypedFormBuilder, + private widgetService: WidgetService) { + } + + ngOnInit(): void { + this.panelTitle = this.isAdd ? 'scada.property.add-property' : 'scada.property.property-settings'; + this.propertyType = this.property.type; + this.propertyFormGroup = this.fb.group( + { + id: [this.property.id, [Validators.required]], + name: [this.property.name, [Validators.required]], + type: [this.property.type, [Validators.required]], + default: [this.property.default, []], + required: [this.property.required, []], + subLabel: [this.property.subLabel, []], + divider: [this.property.divider, []], + fieldSuffix: [this.property.fieldSuffix, []], + disableOnProperty: [this.property.disableOnProperty, []], + rowClass: [(this.property.rowClass || '').split(' '), []], + fieldClass: [(this.property.fieldClass || '').split(' '), []], + min: [this.property.min, []], + max: [this.property.max, []], + step: [this.property.step, [Validators.min(0)]] + } + ); + this.propertyFormGroup.get('type').valueChanges.subscribe(() => { + this.updateValidators(); + }); + this.updateValidators(); + } + + cancel() { + this.popover?.hide(); + } + + applyPropertySettings() { + const property = this.propertyFormGroup.getRawValue(); + property.rowClass = (property.rowClass || []).join(' '); + property.fieldClass = (property.fieldClass || []).join(' '); + this.propertySettingsApplied.emit(property); + } + + private updateValidators() { + const type: IotSvgPropertyType = this.propertyFormGroup.get('type').value; + if (type === IotSvgPropertyType.number) { + this.propertyFormGroup.get('min').enable({emitEvent: false}); + this.propertyFormGroup.get('max').enable({emitEvent: false}); + this.propertyFormGroup.get('step').enable({emitEvent: false}); + } else { + this.propertyFormGroup.get('min').disable({emitEvent: false}); + this.propertyFormGroup.get('max').disable({emitEvent: false}); + this.propertyFormGroup.get('step').disable({emitEvent: false}); + } + if (this.propertyType !== type) { + const defaultValue = defaultPropertyValue(type); + this.propertyFormGroup.get('default').patchValue(defaultValue, {emitEvent: false}); + this.propertyType = type; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html new file mode 100644 index 0000000000..7016461cce --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html @@ -0,0 +1,49 @@ + + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss new file mode 100644 index 0000000000..5c0b4f0166 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-scada-symbol-metadata-property-row { + .tb-id-field { + flex: 1 1 40%; + } + .tb-name-field { + flex: 1 1 40%; + } + .tb-type-field { + flex: 1 1 20%; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts new file mode 100644 index 0000000000..60a56aae50 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts @@ -0,0 +1,280 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + Renderer2, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validator, + ValidatorFn, + Validators +} from '@angular/forms'; +import { + IotSvgProperty, + IotSvgPropertyType, + iotSvgPropertyTypes, + iotSvgPropertyTypeTranslations +} from '@home/components/widget/lib/svg/iot-svg.models'; +import { deepClone } from '@core/utils'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { constantColor, Font } from '@shared/models/widget-settings.models'; +import { + ScadaSymbolPropertyPanelComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component'; +import { + ScadaSymbolPropertiesComponent +} from '@home/pages/scada-symbol/metadata-components/scada-symbol-properties.component'; + +export const propertyValid = (property: IotSvgProperty): boolean => !(!property.id || !property.name || !property.type); + +export const defaultPropertyValue = (type: IotSvgPropertyType): any => { + switch (type) { + case IotSvgPropertyType.text: + return ''; + case IotSvgPropertyType.number: + return 0; + case IotSvgPropertyType.switch: + return false; + case IotSvgPropertyType.color: + return '#000'; + case IotSvgPropertyType.color_settings: + return constantColor('#000'); + case IotSvgPropertyType.font: + return { + size: 12, + sizeUnit: 'px', + family: 'Roboto', + weight: 'normal', + style: 'normal', + lineHeight: '1' + } as Font; + case IotSvgPropertyType.units: + return ''; + } +}; + +@Component({ + selector: 'tb-scada-symbol-metadata-property-row', + templateUrl: './scada-symbol-property-row.component.html', + styleUrls: ['./scada-symbol-property-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, OnInit, Validator { + + @ViewChild('idInput') + idInput: ElementRef; + + @ViewChild('editButton') + editButton: MatButton; + + iotSvgPropertyTypes = iotSvgPropertyTypes; + iotSvgPropertyTypeTranslations = iotSvgPropertyTypeTranslations; + + @Input() + disabled: boolean; + + @Input() + index: number; + + @Input() + booleanPropertyIds: string[]; + + @Output() + propertyRemoved = new EventEmitter(); + + propertyRowFormGroup: UntypedFormGroup; + + modelValue: IotSvgProperty; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private propertiesComponent: ScadaSymbolPropertiesComponent) { + } + + ngOnInit() { + this.propertyRowFormGroup = this.fb.group({ + id: [null, [this.propertyIdValidator()]], + name: [null, [Validators.required]], + type: [null, [Validators.required]] + }); + this.propertyRowFormGroup.valueChanges.subscribe( + () => this.updateModel() + ); + this.propertyRowFormGroup.get('type').valueChanges.subscribe((newType: IotSvgPropertyType) => { + this.onTypeChanged(newType); + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.propertyRowFormGroup.disable({emitEvent: false}); + } else { + this.propertyRowFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: IotSvgProperty): void { + this.modelValue = value; + this.propertyRowFormGroup.patchValue( + { + id: value?.id, + name: value?.name, + type: value?.type + }, {emitEvent: false} + ); + this.cd.markForCheck(); + } + + editProperty($event: Event, matButton: MatButton, add = false, editCanceled = () => {}) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const ctx: any = { + isAdd: add, + booleanPropertyIds: this.booleanPropertyIds, + property: deepClone(this.modelValue) + }; + const scadaSymbolPropertyPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, ScadaSymbolPropertyPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null, + ctx, + {}, + {}, {}, true); + scadaSymbolPropertyPanelPopover.tbComponentRef.instance.popover = scadaSymbolPropertyPanelPopover; + scadaSymbolPropertyPanelPopover.tbComponentRef.instance.propertySettingsApplied.subscribe((property) => { + scadaSymbolPropertyPanelPopover.hide(); + this.propertyRowFormGroup.patchValue( + { + id: property.id, + name: property.name, + type: property.type + }, {emitEvent: false} + ); + this.modelValue = property; + this.propagateChange(this.modelValue); + }); + scadaSymbolPropertyPanelPopover.tbDestroy.subscribe(() => { + if (!propertyValid(this.modelValue)) { + editCanceled(); + } + }); + } + } + + focus() { + this.idInput.nativeElement.scrollIntoView(); + this.idInput.nativeElement.focus(); + } + + onAdd(onCanceled: () => void) { + this.idInput.nativeElement.scrollIntoView(); + this.editProperty(null, this.editButton, true, onCanceled); + } + + public validate(c: UntypedFormControl) { + const idControl = this.propertyRowFormGroup.get('id'); + if (idControl.hasError('propertyIdNotUnique')) { + idControl.updateValueAndValidity({onlySelf: false, emitEvent: false}); + } + if (idControl.hasError('propertyIdNotUnique')) { + this.propertyRowFormGroup.get('id').markAsTouched(); + return { + propertyIdNotUnique: true + }; + } + const property: IotSvgProperty = {...this.modelValue, ...this.propertyRowFormGroup.value}; + if (!propertyValid(property)) { + return { + property: true + }; + } + return null; + } + + private propertyIdValidator(): ValidatorFn { + return control => { + if (!control.value) { + return { + required: true + }; + } + if (!this.propertiesComponent.propertyIdUnique(control.value, this.index)) { + return { + propertyIdNotUnique: true + }; + } + return null; + }; + } + + private onTypeChanged(newType: IotSvgPropertyType) { + this.modelValue = {...this.modelValue, ...{type: newType}}; + this.modelValue.default = defaultPropertyValue(newType); + } + + private updateModel() { + const value: IotSvgProperty = this.propertyRowFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts index c595f63071..086ca454d7 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol.component.ts @@ -320,6 +320,9 @@ export class ScadaSymbolComponent extends PageComponent } private reset(): void { + if (this.symbolMetadata) { + this.symbolMetadata.selectedOption = 'general'; + } this.previewMode = false; } 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 42c5a644c2..1d49829f21 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3439,7 +3439,8 @@ "true-label": "True label", "false-label": "False label", "state-label": "State label", - "default-payload": "Default payload" + "default-payload": "Default payload", + "not-unique-behavior-ids-error": "Behavior Ids must be unique!" }, "property": { "property": "Property", @@ -3456,7 +3457,21 @@ "no-properties": "No properties configured", "add-property": "Add property", "property-settings": "Property settings", - "remove-property": "Remove property" + "remove-property": "Remove property", + "default-value": "Default value", + "value-required": "Value required", + "number-settings": "Number settings", + "min": "Min", + "max": "Max", + "step": "Step", + "advanced-ui-settings": "Advanced UI settings", + "disable-on-property": "Disable on property", + "sub-label": "Sub label", + "vertical-divider-after": "Vertical divider after", + "input-field-suffix": "Input field suffix", + "property-row-classes": "Property row classes", + "property-field-classes": "Property field classes", + "not-unique-property-ids-error": "Property Ids must be unique!" } }, "item": {