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 {