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 cfdcfacfc4..5a37c43486 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 @@ -21,7 +21,36 @@
dynamic-form.property.id
dynamic-form.property.name
dynamic-form.property.type
-
+
+ + + +
{ - let properties: FormProperty[] = this.propertiesFormGroup.get('properties').value; - if (properties) { - properties = properties.filter(p => propertyValid(p)); - } + const properties = this.getProperties(); this.booleanPropertyIds = properties.filter(p => p.type === FormPropertyType.switch).map(p => p.id); properties.forEach((p, i) => { if (p.disableOnProperty && !this.booleanPropertyIds.includes(p.disableOnProperty)) { @@ -222,6 +230,38 @@ export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnI }); } + export($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const properties = this.getProperties(); + this.importExportService.exportFormProperties(properties, this.exportFileName); + } + + import($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.importExportService.importFormProperties().subscribe((properties) => { + if (properties) { + this.propertiesFormGroup.setControl('properties', this.preparePropertiesFormArray(properties), {emitEvent: true}); + } + }); + } + + clear($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm(this.translate.instant('dynamic-form.clear-form'), + this.translate.instant('dynamic-form.clear-form-prompt'), null, this.translate.instant('action.clear')) + .subscribe((clear) => { + if (clear) { + (this.propertiesFormGroup.get('properties') as UntypedFormArray).clear({emitEvent: true}); + } + }); + } + private preparePropertiesFormArray(properties: FormProperty[] | undefined): UntypedFormArray { const propertiesControls: Array = []; if (properties) { @@ -231,4 +271,12 @@ export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnI } return this.fb.array(propertiesControls); } + + private getProperties(): FormProperty[] { + let properties: FormProperty[] = this.propertiesFormGroup.get('properties').value; + if (properties) { + properties = properties.filter(p => propertyValid(p)); + } + return properties; + } } 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 c10aaaf803..5e8f9b9928 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 @@ -267,6 +267,22 @@ {{ 'dynamic-form.property.allow-empty-select-option' | translate }}
+
+
dynamic-form.property.selected-options-limit
+
+
dynamic-form.property.min
+ + + + +
dynamic-form.property.max
+ + + +
+
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 ee19d25a3d..960efa55fc 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 @@ -124,6 +124,8 @@ export class DynamicFormPropertyPanelComponent implements OnInit { properties: [this.property.properties, []], multiple: [this.property.multiple, []], allowEmptyOption: [this.property.allowEmptyOption, []], + minItems: [this.property.minItems, []], + maxItems: [this.property.maxItems, []], items: [this.property.items, []], helpId: [this.property.helpId, []], direction: [this.property.direction || 'column', []], @@ -208,13 +210,19 @@ export class DynamicFormPropertyPanelComponent implements OnInit { const multiple: boolean = this.propertyFormGroup.get('multiple').value; if (multiple) { this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false}); + this.propertyFormGroup.get('minItems').enable({emitEvent: false}); + this.propertyFormGroup.get('maxItems').enable({emitEvent: false}); } else { this.propertyFormGroup.get('allowEmptyOption').enable({emitEvent: false}); + this.propertyFormGroup.get('minItems').disable({emitEvent: false}); + this.propertyFormGroup.get('maxItems').disable({emitEvent: false}); } } } else { this.propertyFormGroup.get('multiple').disable({emitEvent: false}); this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false}); + this.propertyFormGroup.get('minItems').disable({emitEvent: false}); + this.propertyFormGroup.get('maxItems').disable({emitEvent: false}); this.propertyFormGroup.get('items').disable({emitEvent: false}); } if (type === FormPropertyType.datetime) { @@ -290,7 +298,9 @@ export class DynamicFormPropertyPanelComponent implements OnInit { this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false}); } this.propertyFormGroup.get('allowEmptyOption').patchValue(false, {emitEvent: false}); - this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false}); + this.propertyFormGroup.get('allowEmptyOption').disable({emitEvent: false}) + this.propertyFormGroup.get('minItems').enable({emitEvent: false}); + this.propertyFormGroup.get('maxItems').enable({emitEvent: false}); } else { if (defaultValue && Array.isArray(defaultValue)) { const newVal = defaultValue.length ? defaultValue[0] : null; @@ -299,6 +309,8 @@ export class DynamicFormPropertyPanelComponent implements OnInit { }); } this.propertyFormGroup.get('allowEmptyOption').enable({emitEvent: false}); + this.propertyFormGroup.get('minItems').disable({emitEvent: false}); + this.propertyFormGroup.get('maxItems').disable({emitEvent: false}); } } } 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 c617ef23d5..e598fd02db 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 @@ -88,7 +88,7 @@ -
+
{{ propertyRow.label | customTranslate }} 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 3c9d7a1f83..432bacf4f1 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 @@ -215,6 +215,16 @@ export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAcce validators.push(Validators.max(property.max)); } } + if (property.type === FormPropertyType.select) { + if (property.multiple) { + if (isDefinedAndNotNull(property.minItems)) { + validators.push(Validators.minLength(property.minItems)); + } + if (isDefinedAndNotNull(property.maxItems)) { + validators.push(Validators.maxLength(property.maxItems)); + } + } + } this.propertiesFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html index 00a18cc2ab..f90a5bef64 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html @@ -107,6 +107,8 @@
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 a5d38991b9..5846f98d0c 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 @@ -90,7 +90,7 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni @Input() tags: string[]; - private modelValue: ScadaSymbolMetadata; + modelValue: ScadaSymbolMetadata; private propagateChange = null; diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index 6f77256e9e..02f0ecead8 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -188,9 +188,11 @@
- +
diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 5b707d6399..7620249034 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -87,6 +87,7 @@ import { ExportResourceDialogData, ExportResourceDialogDialogResult } from '@shared/import-export/export-resource-dialog.component'; +import { FormProperty, propertyValid } from '@shared/models/dynamic-form.models'; export type editMissingAliasesFunction = (widgets: Array, isSingleWidget: boolean, customTitle: string, missingEntityAliases: EntityAliases) => Observable; @@ -119,6 +120,26 @@ export class ImportExportService { } + public exportFormProperties(properties: FormProperty[], fileName: string) { + this.exportToPc(properties, fileName); + } + + public importFormProperties(): Observable { + return this.openImportDialog('dynamic-form.import-form', 'dynamic-form.form-json-file').pipe( + map((properties: FormProperty[]) => { + if (!this.validateImportedFormProperties(properties)) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('dynamic-form.invalid-form-json-file-error'), + type: 'error'})); + throw new Error('Invalid form JSON file'); + } else { + return properties; + } + }), + catchError(() => of(null)) + ); + } + public exportImage(type: ImageResourceType, key: string) { this.imageService.exportImage(type, key).subscribe( { @@ -927,6 +948,14 @@ export class ImportExportService { type: 'error'})); } + private validateImportedFormProperties(properties: FormProperty[]): boolean { + if (!properties.length) { + return false; + } else { + return !properties.some(p => !propertyValid(p)); + } + } + private validateImportedImage(image: ImageExportData): boolean { return !(!isNotEmptyStr(image.data) || !isNotEmptyStr(image.title) 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 773f6bdebe..14df1ff3b2 100644 --- a/ui-ngx/src/app/shared/models/dynamic-form.models.ts +++ b/ui-ngx/src/app/shared/models/dynamic-form.models.ts @@ -132,6 +132,8 @@ export interface FormSelectProperty extends FormPropertyBase { multiple?: boolean; allowEmptyOption?: boolean; items?: FormSelectItem[]; + minItems?: number; + maxItems?: number; } export type FormPropertyDirection = 'row' | 'column'; @@ -311,6 +313,11 @@ const toPropertyContainers = (properties: FormProperty[], visible: true }; result.push(propertyRow); + const rowClasses = (propertyRow.rowClass || '').split(' ').filter(cls => cls.trim().length > 0); + if (!rowClasses.includes('flex-wrap')) { + rowClasses.push('flex-wrap'); + } + propertyRow.rowClass = rowClasses.join(' '); } if (property.type === FormPropertyType.switch) { propertyRow.switch = property; @@ -330,10 +337,18 @@ const toPropertyContainers = (properties: FormProperty[], 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'; + container.propertiesRowClass = 'gt-xs:align-center xs:flex-col gt-xs:flex-row gt-xs:justify-end'; + const rowClasses = (container.rowClass || '').split(' ').filter(cls => cls.trim().length > 0); + if (!rowClasses.includes('column-xs')) { + rowClasses.push('column-xs'); } + if (property.fieldClass && property.fieldClass.split(' ').includes('flex')) { + container.propertiesRowClass += ' overflow-hidden'; + if (rowClasses.includes('flex-wrap')) { + rowClasses.splice(rowClasses.indexOf('flex-wrap'), 1); + } + } + container.rowClass = rowClasses.join(' '); } } } @@ -600,7 +615,7 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: name: form.title || id, group: groupTitle, type: null, - default: form.schema?.default || null, + default: (isDefinedAndNotNull(form.default) ? form.default : form.schema?.default) || null, required: isDefinedAndNotNull(form.required) ? form.required : false }; if (form.condition?.length) { @@ -635,6 +650,14 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: property.multiple = form.multiple; property.fieldClass = 'flex'; property.allowEmptyOption = isDefinedAndNotNull(form.allowClear) ? form.allowClear : false; + if (property.multiple) { + if (typeof (form.schema as any)?.minItems === 'number') { + property.minItems = (form.schema as any).minItems; + } + if (typeof (form.schema as any)?.maxItems === 'number') { + property.maxItems = (form.schema as any).maxItems; + } + } break; case 'select': property.type = FormPropertyType.select; @@ -699,17 +722,25 @@ const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: 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; + const arrayItemSchema = form.schema.items; + if (arrayItemSchema && arrayItemSchema.type && arrayItemSchema.type !== 'array') { + if (arrayItemSchema.type === 'object') { + property.arrayItemType = FormPropertyType.fieldset; + property.arrayItemName = ''; + property.properties = form.items ? (form.items as JsonFormData[]).map(item => + jsonFormDataToProperty(item, level+2)).filter(p => p !== null) : []; + } else { + const item: JsonFormData = form.items[0] as JsonFormData; + 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; } } 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 58e947e80e..a8ed773047 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1657,6 +1657,7 @@ "min": "Min", "max": "Max", "step": "Step", + "selected-options-limit": "Selected options limit", "advanced-ui-settings": "Advanced UI settings", "disable-on-property": "Disable on property", "display-condition-function": "Display condition function", @@ -1693,7 +1694,13 @@ "item-type": "Item type", "item-name": "Item name", "no-items": "No items" - } + }, + "clear-form": "Clear form", + "clear-form-prompt": "Are you sure you want to remove all form properties?", + "import-form": "Import form from JSON", + "export-form": "Export form to JSON", + "form-json-file": "Form JSON file", + "invalid-form-json-file-error": "Unable to import form from JSON: Invalid form JSON data structure." }, "asset-profile": { "asset-profile": "Asset profile", @@ -5473,7 +5480,7 @@ "html": "HTML", "tidy": "Tidy", "css": "CSS", - "settings-form-properties": "Settings form properties", + "settings-form": "Settings form", "settings-schema": "Settings schema", "datakey-settings-schema": "Data key settings schema", "latest-datakey-settings-schema": "Latest data key settings schema", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 14a3455685..0ae2b9584b 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -223,7 +223,7 @@ } .mat-divider-vertical { height: 56px; - margin-top: -7px; + margin-top: -9px; margin-bottom: -7px; } .mat-mdc-form-field, tb-unit-input {