diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts index 262d6c57a7..d7b5c2f7a1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts @@ -37,7 +37,12 @@ import { } from '@angular/forms'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { TranslateService } from '@ngx-translate/core'; -import { FormProperty, FormPropertyType, propertyValid } from '@shared/models/dynamic-form.models'; +import { + cleanupFormProperties, + FormProperty, + FormPropertyType, + propertyValid +} from '@shared/models/dynamic-form.models'; import { DynamicFormPropertyRowComponent } from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component'; @@ -134,7 +139,7 @@ export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnI controls[i].patchValue(p, {emitEvent: false}); } }); - this.propagateChange(properties); + this.propagateChange(cleanupFormProperties(properties)); } ); } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts index b8175704ca..eb4783b095 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts @@ -54,8 +54,24 @@ const widgetEditorCompletions = (settingsCompletions?: TbEditorCompletions): TbE description: 'Called when widget element is destroyed. Should be used to cleanup all resources if necessary.', meta: 'function' }, + getSettingsForm: { + description: 'Optional function returning widget settings form array as alternative to Settings form tab of settings section.', + meta: 'function', + return: { + description: 'An array of widget settings form properties', + type: 'Array<FormProperty>' + } + }, + getDataKeySettingsForm: { + description: 'Optional function returning particular data key settings form array as alternative to Data key settings form tab of settings section.', + meta: 'function', + return: { + description: 'An array of data key settings form properties', + type: 'Array<FormProperty>' + } + }, getSettingsSchema: { - description: 'Optional function returning widget settings schema json as alternative to Settings tab of Settings schema section.', + description: 'Deprecated. Use getSettingsForm() function.', meta: 'function', return: { description: 'An widget settings schema json', @@ -63,7 +79,7 @@ const widgetEditorCompletions = (settingsCompletions?: TbEditorCompletions): TbE } }, getDataKeySettingsSchema: { - description: 'Optional function returning particular data key settings schema json as alternative to Data key settings schema of Settings schema section.', + description: 'Deprecated. Use getDataKeySettingsForm() function.', meta: 'function', return: { description: 'A particular data key settings schema json', diff --git a/ui-ngx/src/app/shared/import-export/import-dialog.component.html b/ui-ngx/src/app/shared/import-export/import-dialog.component.html index c472fb2526..8bbe1599c4 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog.component.html +++ b/ui-ngx/src/app/shared/import-export/import-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ importTitle }}

@@ -30,15 +30,28 @@
- + {{ importFileLabel | translate }} + {{ importContentLabel | translate }} + + + +
diff --git a/ui-ngx/src/app/shared/import-export/import-dialog.component.ts b/ui-ngx/src/app/shared/import-export/import-dialog.component.ts index b68fe3b2cd..e4256229c4 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog.component.ts +++ b/ui-ngx/src/app/shared/import-export/import-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { Component, DestroyRef, Inject, OnInit, SkipSelf } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; @@ -23,10 +23,14 @@ import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDire import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { isDefinedAndNotNull } from '@core/utils'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export interface ImportDialogData { importTitle: string; importFileLabel: string; + enableImportFromContent?: boolean; + importContentLabel?: string; } @Component({ @@ -40,9 +44,13 @@ export class ImportDialogComponent extends DialogComponent, @@ -50,17 +58,26 @@ export class ImportDialogComponent extends DialogComponent, + private destroyRef: DestroyRef, private fb: UntypedFormBuilder) { super(store, router, dialogRef); this.importTitle = data.importTitle; this.importFileLabel = data.importFileLabel; - - this.importFormGroup = this.fb.group({ - jsonContent: [null, [Validators.required]] - }); + this.enableImportFromContent = isDefinedAndNotNull(data.enableImportFromContent) ? data.enableImportFromContent : false; + this.importContentLabel = data.importContentLabel; } ngOnInit(): void { + this.importFormGroup = this.fb.group({ + importType: ['file'], + fileContent: [null, [Validators.required]], + jsonContent: [null, [Validators.required]] + }); + this.importFormGroup.get('importType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.importTypeChanged(); + }); } isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { @@ -85,7 +102,19 @@ export class ImportDialogComponent extends DialogComponent { - return this.openImportDialog('dynamic-form.import-form', 'dynamic-form.form-json-file').pipe( + return this.openImportDialog('dynamic-form.import-form', + 'dynamic-form.json-file', true, 'dynamic-form.json-content').pipe( map((properties: FormProperty[]) => { if (!this.validateImportedFormProperties(properties)) { this.store.dispatch(new ActionNotificationShow( @@ -1134,14 +1135,17 @@ export class ImportExportService { }; } - private openImportDialog(importTitle: string, importFileLabel: string): Observable { + private openImportDialog(importTitle: string, importFileLabel: string, + enableImportFromContent = false, importContentLabel?: string): Observable { return this.dialog.open(ImportDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { importTitle, - importFileLabel + importFileLabel, + enableImportFromContent, + importContentLabel } }).afterClosed().pipe( map((importedData) => { diff --git a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts index 4370a2cd49..d6a2c0a25f 100644 --- a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts @@ -325,7 +325,7 @@ export const widgetContextCompletionsWithSettings = (settingsCompletions?: TbEdi type: 'object' }, settings: { - description: 'Widget settings containing widget specific properties according to the defined settings json schema', + description: 'Widget settings containing widget specific properties according to the defined settings form.', meta: 'property', type: 'object', children: settingsCompletions @@ -363,7 +363,7 @@ export const widgetContextCompletionsWithSettings = (settingsCompletions?: TbEdi } }, settings: { - description: 'Widget settings containing widget specific properties according to the defined settings json schema', + description: 'Widget settings containing widget specific properties according to the defined settings form.', meta: 'property', type: 'object', children: settingsCompletions 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 bb7f134c28..d0ee306f89 100644 --- a/ui-ngx/src/app/shared/models/dynamic-form.models.ts +++ b/ui-ngx/src/app/shared/models/dynamic-form.models.ts @@ -16,8 +16,8 @@ import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models'; -import { deepClone, isDefinedAndNotNull, isString } from '@core/utils'; -import { JsonSchema, JsonSettingsSchema, JsonFormData, KeyLabelItem } from '@shared/legacy/json-form.models'; +import { deepClone, isDefinedAndNotNull, isEmptyStr, isString, isUndefinedOrNull } from '@core/utils'; +import { JsonFormData, JsonSchema, JsonSettingsSchema, KeyLabelItem } from '@shared/legacy/json-form.models'; import JsonFormUtils from '@shared/legacy/json-form-utils'; import { constantColor, Font } from '@shared/models/widget-settings.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @@ -165,6 +165,63 @@ export interface FormHtmlSection extends FormPropertyBase { export type FormProperty = FormPropertyBase & FormTextareaProperty & FormNumberProperty & FormSelectProperty & FormRadiosProperty & FormDateTimeProperty & FormJavascriptProperty & FormMarkdownProperty & FormFieldSetProperty & FormArrayProperty & FormHtmlSection; +export const cleanupFormProperties = (properties: FormProperty[]): FormProperty[] => { + for (const property of properties) { + cleanupFormProperty(property); + } + return properties; +} + +export const cleanupFormProperty = (property: FormProperty): FormProperty => { + if (property.type !== FormPropertyType.number) { + delete property.min; + delete property.max; + delete property.step; + } + if (property.type !== FormPropertyType.textarea) { + delete property.rows; + } + if (property.type !== FormPropertyType.fieldset) { + delete property.properties; + } else if (property.properties?.length) { + property.properties = cleanupFormProperties(property.properties); + } + if (property.type !== FormPropertyType.array) { + delete property.arrayItemName; + delete property.arrayItemType; + } + if (property.type !== FormPropertyType.select) { + delete property.multiple; + delete property.allowEmptyOption; + delete property.minItems; + delete property.maxItems; + } + if (property.type !== FormPropertyType.radios) { + delete property.direction; + } + if (![FormPropertyType.select, FormPropertyType.radios].includes(property.type)) { + delete property.items; + } + if (property.type !== FormPropertyType.datetime) { + delete property.allowClear; + delete property.dateTimeType; + } + if (![FormPropertyType.javascript, FormPropertyType.markdown].includes(property.type)) { + delete property.helpId; + } + if (property.type !== FormPropertyType.htmlSection) { + delete property.htmlClassList; + delete property.htmlContent; + } + for (const key of Object.keys(property)) { + const val = property[key]; + if (isUndefinedOrNull(val) || isEmptyStr(val)) { + delete property[key]; + } + } + return property; +} + export enum FormPropertyContainerType { field = 'field', row = 'row', diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md index c0126214c7..9853bebd43 100644 --- a/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md @@ -40,32 +40,25 @@ The **Widget Editor** will be opened, pre-populated with the content of the defa {:copy-code} ``` - - Put the following JSON content inside the "Settings schema" tab of **Settings schema section**: + - Import the following JSON content inside the "Settings form" tab by clicking on 'Import form from JSON' button: ```json -{ - "schema": { - "type": "object", - "title": "AlarmTableSettings", - "properties": { - "alarmSeverityColorFunction": { - "title": "Alarm severity color function: f(severity)", - "type": "string", - "default": "if(severity == 'CRITICAL') {return 'red';} else if (severity == 'MAJOR') {return 'orange';} else return 'green'; " - } - }, - "required": [] - }, - "form": [ - { - "key": "alarmSeverityColorFunction", - "type": "javascript" - } - ] -} +[ + { + "id": "alarmSeverityColorFunction", + "name": "Alarm severity color function: f(severity)", + "type": "javascript", + "default": "if (severity == 'CRITICAL') {\n return 'red';\n} else if (severity == 'MAJOR') {\n return 'orange';\n} else return 'green';", + "required": false + } +] {:copy-code} ``` + - Clear all 'form selector' fields in the "Widget settings" tab. + + - Turn off 'Has basic mode' switch in the "Widget settings" tab. + - Put the following JavaScript code inside the "JavaScript" section: ```javascript diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md index 44abed8a46..7c7121cd44 100644 --- a/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md @@ -40,35 +40,28 @@ The **Widget Editor** will open, pre-populated with default **Control** template {:copy-code} ``` - - Put the following JSON content inside the "Settings schema" tab of **Settings schema section**: + - Import the following JSON content inside the "Settings form" tab by clicking on 'Import form from JSON' button: ```json -{ - "schema": { - "type": "object", - "title": "Settings", - "properties": { - "oneWayElseTwoWay": { - "title": "Is One Way Command", - "type": "boolean", - "default": true - }, - "requestTimeout": { - "title": "RPC request timeout", - "type": "number", - "default": 500 - } - }, - "required": [] - }, - "form": [ - "oneWayElseTwoWay", - "requestTimeout" - ] -} +[ + { + "id": "oneWayElseTwoWay", + "name": "Is One Way Command", + "type": "switch", + "default": true + }, + { + "id": "requestTimeout", + "name": "RPC request timeout", + "type": "number", + "default": 500 + } +] {:copy-code} ``` + - Clear value of 'Settings form selector' in the "Widget settings" tab. + - Put the following JavaScript code inside the "JavaScript" section: ```javascript diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md index d7c7901052..3a3bfe115d 100644 --- a/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md @@ -17,28 +17,23 @@ The **Widget Editor** will be opened pre-populated with the content of default * {:copy-code} ``` - - Put the following JSON content inside the "Settings schema" tab of **Settings schema section**: + - Import the following JSON content inside the "Settings form" tab by clicking on 'Import form from JSON' button: ```json -{ - "schema": { - "type": "object", - "title": "Settings", - "properties": { - "alertContent": { - "title": "Alert content", - "type": "string", - "default": "Content derived from alertContent property of widget settings." - } - } - }, - "form": [ - "alertContent" - ] -} +[ + { + "id": "alertContent", + "name": "Alert content", + "type": "text", + "default": "Content derived from alertContent property of widget settings.", + "fieldClass": "flex" + } +] {:copy-code} ``` + - Clear value of 'Settings form selector' in the "Widget settings" tab. + - Put the following JavaScript code inside the "JavaScript" section: ```javascript diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md index 32b005bedd..015bac28b4 100644 --- a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md @@ -10,18 +10,20 @@ Each widget function should be defined as a property of the **self** variable. In order to implement a new widget, the following JavaScript functions should be defined *(Note: each function is optional and can be implemented according to widget specific behaviour):* -|{:auto} **Function** | **Description** | -|------------------------------------|----------------------------------------------------------------------------------------| -| ``` onInit() ``` | The first function which is called when widget is ready for initialization. Should be used to prepare widget DOM, process widget settings and initial subscription information. | -| ``` onDataUpdated() ``` | Called when the new data is available from the widget subscription. Latest data can be accessed from the object of widget context (**ctx**). | -| ``` onResize() ``` | Called when widget container is resized. Latest width and height can be obtained from widget context (**ctx**). | -| ``` onEditModeChanged() ``` | Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of **ctx**. | -| ``` onMobileModeChanged() ``` | Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of **ctx**. | -| ``` onDestroy() ``` | Called when widget element is destroyed. Should be used to cleanup all resources if necessary. | -| ``` getSettingsSchema() ``` | Optional function returning widget settings schema json as alternative to **Settings schema** of settings section. | -| ``` getDataKeySettingsSchema() ``` | Optional function returning particular data key settings schema json as alternative to **Data key settings schema** tab of settings section. | -| ``` typeParameters() ``` | Returns [WidgetTypeParameters{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L151) object describing widget datasource parameters. See | | -| ``` actionSources() ``` | Returns map describing available widget action sources ([WidgetActionSource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L121)) used to define user actions. See | +| {:auto} **Function** | **Description** | +|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ``` onInit() ``` | The first function which is called when widget is ready for initialization. Should be used to prepare widget DOM, process widget settings and initial subscription information. | +| ``` onDataUpdated() ``` | Called when the new data is available from the widget subscription. Latest data can be accessed from the object of widget context (**ctx**). | +| ``` onResize() ``` | Called when widget container is resized. Latest width and height can be obtained from widget context (**ctx**). | +| ``` onEditModeChanged() ``` | Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of **ctx**. | +| ``` onMobileModeChanged() ``` | Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of **ctx**. | +| ``` onDestroy() ``` | Called when widget element is destroyed. Should be used to cleanup all resources if necessary. | +| ``` getSettingsForm() ``` | Optional function returning widget settings form array as alternative to **Settings form** tab of settings section. | +| ``` getDataKeySettingsForm() ``` | Optional function returning particular data key settings form array as alternative to **Data key settings form** tab of settings section. | +| ``` typeParameters() ``` | Returns [WidgetTypeParameters{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L151) object describing widget datasource parameters. See | | +| ``` actionSources() ``` | Returns map describing available widget action sources ([WidgetActionSource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L121)) used to define user actions. See | +| ~~getSettingsSchema()~~ | **Deprecated**. Use getSettingsForm() function. | +| ~~getDataKeySettingsSchema()~~ | **Deprecated**. Use getDataKeySettingsForm() function. |
diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md index edef68210d..dbf4d991f8 100644 --- a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md +++ b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md @@ -26,7 +26,7 @@ For [Latest values{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/us label: 'Sin', // label of the dataKey. Used as display value (for ex. in the widget legend section) color: '#ffffff', // color of the key. Can be used by widget to set color of the key data (for ex. lines in line chart or segments in the pie chart). funcBody: "", // only applicable for datasource with type "function" and "function" key type. Defines body of the function to generate simulated data. - settings: {} // dataKey specific settings with structure according to the defined Data key settings json schema. See "Settings schema section". + settings: {} // dataKey specific settings with structure according to the defined Data key settings form. }, //... ] @@ -72,7 +72,7 @@ For [Alarm widget{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/use type: 'alarm', // type of the dataKey. Only "alarm" in this case. label: 'Severity', // label of the dataKey. Used as display value (for ex. as a column title in the Alarms table) color: '#ffffff', // color of the key. Can be used by widget to set color of the key data. - settings: {} // dataKey specific settings with structure according to the defined Data key settings json schema. See "Settings schema section". + settings: {} // dataKey specific settings with structure according to the defined Data key settings form. }, //... ] 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 875613fb6a..6c4614abbb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1699,7 +1699,8 @@ "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", + "json-file": "JSON file", + "json-content": "JSON content", "invalid-form-json-file-error": "Unable to import form from JSON: Invalid form JSON data structure." }, "asset-profile": {