Browse Source

Merge pull request #12325 from thingsboard/feature/dynamic-form

Implement dynamic forms to replace JSON schema forms
pull/12340/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
e23ed643ab
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      ui-ngx/angular.json
  2. 21
      ui-ngx/package.json
  3. 3
      ui-ngx/src/app/core/http/widget.service.ts
  4. 15
      ui-ngx/src/app/core/services/utils.service.ts
  5. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  6. 37
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  7. 38
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  8. 3
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html
  9. 3
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
  10. 3
      ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html
  11. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts
  12. 22
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  13. 9
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  14. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html
  15. 7
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts
  16. 15
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  17. 5
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts
  18. 11
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts
  19. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  20. 13
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  21. 13
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  22. 2
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  23. 5
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html
  24. 24
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts
  25. 13
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  26. 182
      ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts
  27. 74
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html
  28. 72
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.scss
  29. 161
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts
  30. 93
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html
  31. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.scss
  32. 129
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts
  33. 471
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html
  34. 31
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss
  35. 317
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts
  36. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html
  37. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.scss
  38. 98
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts
  39. 37
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html
  40. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.scss
  41. 171
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts
  42. 33
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html
  43. 38
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.scss
  44. 204
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts
  45. 266
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html
  46. 40
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.scss
  47. 301
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts
  48. 57
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html
  49. 95
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts
  50. 32
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models.ts
  51. 37
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  52. 69
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  53. 46
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  54. 40
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  55. 18
      ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html
  56. 1
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.html
  57. 4
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts
  58. 14
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts
  59. 6
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html
  60. 2
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts
  61. 190
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html
  62. 140
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts
  63. 43
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts
  64. 69
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  65. 135
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  66. 161
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts
  67. 18
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  68. 2
      ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts
  69. 10
      ui-ngx/src/app/shared/components/js-func.component.ts
  70. 23
      ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts
  71. 23
      ui-ngx/src/app/shared/components/json-form/json-form.component.html
  72. 293
      ui-ngx/src/app/shared/components/json-form/json-form.component.ts
  73. 239
      ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx
  74. 177
      ui-ngx/src/app/shared/components/json-form/react/json-form-array.tsx
  75. 122
      ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx
  76. 46
      ui-ngx/src/app/shared/components/json-form/react/json-form-checkbox.tsx
  77. 186
      ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx
  78. 40
      ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx
  79. 83
      ui-ngx/src/app/shared/components/json-form/react/json-form-date.tsx
  80. 44
      ui-ngx/src/app/shared/components/json-form/react/json-form-fieldset.tsx
  81. 40
      ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx
  82. 162
      ui-ngx/src/app/shared/components/json-form/react/json-form-icon.tsx
  83. 108
      ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx
  84. 40
      ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx
  85. 40
      ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx
  86. 99
      ui-ngx/src/app/shared/components/json-form/react/json-form-number.tsx
  87. 51
      ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx
  88. 202
      ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx
  89. 53
      ui-ngx/src/app/shared/components/json-form/react/json-form-react.tsx
  90. 217
      ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx
  91. 93
      ui-ngx/src/app/shared/components/json-form/react/json-form-select.tsx
  92. 92
      ui-ngx/src/app/shared/components/json-form/react/json-form-text.tsx
  93. 141
      ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts
  94. 361
      ui-ngx/src/app/shared/components/json-form/react/json-form.scss
  95. 46
      ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts
  96. 3
      ui-ngx/src/app/shared/components/string-items-list.component.html
  97. 3
      ui-ngx/src/app/shared/components/string-items-list.component.ts
  98. 46
      ui-ngx/src/app/shared/components/time/datetime.component.html
  99. 60
      ui-ngx/src/app/shared/components/time/datetime.component.scss
  100. 32
      ui-ngx/src/app/shared/components/time/datetime.component.ts

2
ui-ngx/angular.json

@ -99,8 +99,6 @@
"node_modules/jquery.terminal/css/jquery.terminal.min.css",
"node_modules/tooltipster/dist/css/tooltipster.bundle.min.css",
"node_modules/tooltipster/dist/css/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css",
"src/app/shared/components/json-form/react/json-form.scss",
"node_modules/rc-select/assets/index.less",
"node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css",
"node_modules/leaflet/dist/leaflet.css",
"src/app/modules/home/components/widget/lib/maps/markers.scss",

21
ui-ngx/package.json

@ -25,8 +25,6 @@
"@angular/platform-browser-dynamic": "18.2.13",
"@angular/router": "18.2.13",
"@auth0/angular-jwt": "^5.2.0",
"@emotion/react": "11.13.3",
"@emotion/styled": "11.13.0",
"@flowjs/flow.js": "^2.14.1",
"@flowjs/ngx-flow": "18.0.1",
"@geoman-io/leaflet-geoman-free": "2.17.0",
@ -34,12 +32,6 @@
"@mat-datetimepicker/core": "~14.0.0",
"@mdi/svg": "^7.4.47",
"@messageformat/core": "^3.4.0",
"@mui/icons-material": "6.1.2",
"@mui/lab": "6.0.0-beta.10",
"@mui/material": "6.1.2",
"@mui/styles": "6.1.2",
"@mui/system": "6.1.2",
"@mui/x-date-pickers": "7.18.0",
"@ngrx/effects": "^18.1.1",
"@ngrx/store": "^18.1.1",
"@ngrx/store-devtools": "^18.1.1",
@ -85,16 +77,8 @@
"ngx-sharebuttons": "^15.0.6",
"ngx-translate-messageformat-compiler": "^7.0.0",
"objectpath": "^2.0.0",
"prettier": "^2.8.3",
"prop-types": "^15.8.1",
"qrcode": "^1.5.4",
"raphael": "^2.3.0",
"rc-select": "14.15.2",
"react": "18.3.1",
"react-ace": "12.0.0",
"react-dom": "18.3.1",
"react-dropzone": "14.2.9",
"reactcss": "^1.2.3",
"rxjs": "~7.8.1",
"schema-inspector": "^2.1.0",
"screenfull": "^6.0.2",
@ -105,7 +89,6 @@
"tinymce": "~6.8.5",
"tooltipster": "^4.2.8",
"tslib": "^2.7.0",
"tv4": "^1.3.0",
"typeface-roboto": "^1.1.13",
"zone.js": "~0.14.10"
},
@ -137,8 +120,6 @@
"@types/lodash": "^4.17.13",
"@types/node": "~20.17.8",
"@types/raphael": "^2.3.9",
"@types/react": "18.3.10",
"@types/react-dom": "18.3.0",
"@types/systemjs": "6.15.1",
"@types/tinycolor2": "^1.4.6",
"@types/tooltipster": "^0.0.35",
@ -162,8 +143,6 @@
"typescript": "~5.5.4"
},
"resolutions": {
"@types/react": "18.3.10",
"rc-virtual-list": "3.5.2",
"ace-builds": "1.36.5",
"tinymce": "6.8.5",
"rollup": "4.22.4",

3
ui-ngx/src/app/core/http/widget.service.ts

@ -24,7 +24,7 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import {
BaseWidgetType,
DeprecatedFilter,
fullWidgetTypeFqn,
fullWidgetTypeFqn, migrateWidgetTypeToDynamicForms,
WidgetType,
widgetType,
WidgetTypeDetails,
@ -271,6 +271,7 @@ export class WidgetService {
return this.getWidgetType(templateWidgetType.template.fullFqn,
config).pipe(
map((result) => {
result = migrateWidgetTypeToDynamicForms(result);
const widgetInfo = toWidgetInfo(result);
widgetInfo.fullFqn = undefined;
return widgetInfo;

15
ui-ngx/src/app/core/services/utils.service.ts

@ -23,7 +23,6 @@ import {
baseUrl,
createLabelFromDatasource,
deepClone,
deleteNullProperties,
guid,
hashCode,
isDefined,
@ -41,7 +40,6 @@ import { DataKeyType, SharedTelemetrySubscriber } from '@app/shared/models/telem
import { alarmFields, alarmSeverityTranslations, alarmStatusTranslations } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults';
import { Observable } from 'rxjs';
import { publishReplay, refCount } from 'rxjs/operators';
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
@ -51,6 +49,7 @@ import { DatePipe, DOCUMENT } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
import cssjs from '@core/css/css';
import { isNotEmptyTbFunction } from '@shared/models/js-function.models';
import { defaultFormProperties, FormProperty } from '@shared/models/dynamic-form.models';
const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g');
@ -138,10 +137,10 @@ export class UtilsService {
return predefinedFunctions[func];
}
public getDefaultDatasource(dataKeySchema: any): Datasource {
public getDefaultDatasource(dataKeyForm: FormProperty[]): Datasource {
const datasource = deepClone(this.defaultDatasource);
if (isDefined(dataKeySchema)) {
datasource.dataKeys[0].settings = this.generateObjectFromJsonSchema(dataKeySchema);
if (dataKeyForm?.length) {
datasource.dataKeys[0].settings = defaultFormProperties(dataKeyForm);
}
return datasource;
}
@ -189,12 +188,6 @@ export class UtilsService {
return '';
}
public generateObjectFromJsonSchema(schema: any): any {
const obj = jsonSchemaDefaults(schema);
deleteNullProperties(obj);
return obj;
}
public processWidgetException(exception: any): ExceptionData {
const data = this.parseException(exception, -6);
if (data.message?.startsWith('NG0')) {

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -160,7 +160,6 @@ import * as MaterialIconsDialogComponent from '@shared/components/dialog/materia
import * as ColorInputComponent from '@shared/components/color-input.component';
import * as MaterialIconSelectComponent from '@shared/components/material-icon-select.component';
import * as NodeScriptTestDialogComponent from '@shared/components/dialog/node-script-test-dialog.component';
import * as JsonFormComponent from '@shared/components/json-form/json-form.component';
import * as NotificationComponent from '@shared/components/notification/notification.component';
import * as TemplateAutocompleteComponent from '@shared/components/notification/template-autocomplete.component';
import * as ImageInputComponent from '@shared/components/image-input.component';
@ -507,7 +506,6 @@ class ModulesMap implements IModulesMap {
'@shared/components/color-input.component': ColorInputComponent,
'@shared/components/material-icon-select.component': MaterialIconSelectComponent,
'@shared/components/dialog/node-script-test-dialog.component': NodeScriptTestDialogComponent,
'@shared/components/json-form/json-form.component': JsonFormComponent,
'@shared/components/notification/notification.component': NotificationComponent,
'@shared/components/notification/template-autocomplete.component': TemplateAutocompleteComponent,
'@shared/components/image-input.component': ImageInputComponent,

37
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -26,7 +26,7 @@ import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget
import { Dashboard } from '@app/shared/models/dashboard.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models';
import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
@ -101,32 +101,17 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
const widgetInfo = this.data.widgetInfo;
const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
const rawLatestDataKeySettingsSchema = widgetInfo.typeLatestDataKeySettingsSchema || widgetInfo.latestDataKeySettingsSchema;
const settingsForm = widgetInfo.typeSettingsForm?.length ?
widgetInfo.typeSettingsForm : (widgetInfo.settingsForm || []);
const dataKeySettingsForm = widgetInfo.typeDataKeySettingsForm?.length ?
widgetInfo.typeDataKeySettingsForm : (widgetInfo.dataKeySettingsForm || []);
const latestDataKeySettingsForm = widgetInfo.typeLatestDataKeySettingsForm?.length ?
widgetInfo.typeLatestDataKeySettingsForm : (widgetInfo.latestDataKeySettingsForm || []);
const typeParameters = widgetInfo.typeParameters;
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
let settingsSchema;
if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {};
} else {
settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema;
}
let dataKeySettingsSchema;
if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') {
dataKeySettingsSchema = {};
} else {
dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema;
}
let latestDataKeySettingsSchema;
if (!rawLatestDataKeySettingsSchema || rawLatestDataKeySettingsSchema === '') {
latestDataKeySettingsSchema = {};
} else {
latestDataKeySettingsSchema = isString(rawLatestDataKeySettingsSchema) ?
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
}
this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config,
@ -142,9 +127,9 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
typeParameters,
actionSources,
isDataEnabled,
settingsSchema,
dataKeySettingsSchema,
latestDataKeySettingsSchema,
settingsForm,
dataKeySettingsForm,
latestDataKeySettingsForm,
dataKeySettingsFunction,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,

38
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts

@ -24,7 +24,7 @@ import { IAliasController, IStateController } from '@core/api/widget-api.models'
import { Widget, WidgetConfigMode } from '@shared/models/widget.models';
import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigComponentData } from '../../models/widget-component.models';
import { isDefined, isDefinedAndNotNull, isString } from '@core/utils';
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
@ -139,32 +139,18 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
return;
}
const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget);
const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
const rawLatestDataKeySettingsSchema = widgetInfo.typeLatestDataKeySettingsSchema || widgetInfo.latestDataKeySettingsSchema;
const settingsForm = widgetInfo.typeSettingsForm?.length ?
widgetInfo.typeSettingsForm : (widgetInfo.settingsForm || []);
const dataKeySettingsForm = widgetInfo.typeDataKeySettingsForm?.length ?
widgetInfo.typeDataKeySettingsForm : (widgetInfo.dataKeySettingsForm || []);
const latestDataKeySettingsForm = widgetInfo.typeLatestDataKeySettingsForm?.length ?
widgetInfo.typeLatestDataKeySettingsForm : (widgetInfo.latestDataKeySettingsForm || []);
const typeParameters = widgetInfo.typeParameters;
const dataKeySettingsFunction: DataKeySettingsFunction = typeParameters?.dataKeySettingsFunction;
const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
let settingsSchema;
if (!rawSettingsSchema || rawSettingsSchema === '') {
settingsSchema = {};
} else {
settingsSchema = isString(rawSettingsSchema) ? JSON.parse(rawSettingsSchema) : rawSettingsSchema;
}
let dataKeySettingsSchema;
if (!rawDataKeySettingsSchema || rawDataKeySettingsSchema === '') {
dataKeySettingsSchema = {};
} else {
dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema;
}
let latestDataKeySettingsSchema;
if (!rawLatestDataKeySettingsSchema || rawLatestDataKeySettingsSchema === '') {
latestDataKeySettingsSchema = {};
} else {
latestDataKeySettingsSchema = isString(rawLatestDataKeySettingsSchema) ?
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
}
this.widgetConfig = {
widgetName: widgetInfo.widgetName,
config: this.widget.config,
@ -173,9 +159,9 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
typeParameters,
actionSources,
isDataEnabled,
settingsSchema,
dataKeySettingsSchema,
latestDataKeySettingsSchema,
settingsForm,
dataKeySettingsForm,
latestDataKeySettingsForm,
dataKeySettingsFunction,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,

3
ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html

@ -31,8 +31,7 @@
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<tb-datetime formControlName="defaultValue"
dateText="filter.date"
timeText="filter.time"
dateText="{{ 'filter.date' | translate }}"
required [showLabel]="false"></tb-datetime>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN">

3
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html

@ -101,8 +101,7 @@
</ng-template>
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.DATE_TIME">
<tb-datetime formControlName="value"
dateText="filter.date"
timeText="filter.time"
dateText="{{ 'filter.date' | translate }}"
required></tb-datetime>
</ng-template>
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.BOOLEAN">

3
ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html

@ -47,8 +47,7 @@
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<label class="tb-title no-padding tb-required">{{ userInputControl.get('label').value }}</label>
<tb-datetime [formControl]="userInputControl.get('value')"
dateText="filter.date"
timeText="filter.time"
dateText="{{ 'filter.date' | translate }}"
required [showLabel]="false"></tb-datetime>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN">

8
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts

@ -35,7 +35,6 @@ import {
DataKey,
DataKeyConfigMode,
DatasourceType,
JsonSettingsSchema,
Widget,
widgetType
} from '@shared/models/widget.models';
@ -54,6 +53,7 @@ import {
AggregatedValueCardKeySettings
} from '@home/components/widget/lib/cards/aggregated-value-card.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
@ -111,8 +111,8 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn
return this.widgetConfigComponent.widget;
}
get latestDataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema;
get latestDataKeySettingsForm(): FormProperty[] {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsForm;
}
get latestDataKeySettingsDirective(): string {
@ -210,7 +210,7 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn
data: {
dataKey: deepClone(this.modelValue),
dataKeyConfigMode: DataKeyConfigMode.general,
dataKeySettingsSchema: this.latestDataKeySettingsSchema,
dataKeySettingsForm: this.latestDataKeySettingsForm,
dataKeySettingsDirective: this.latestDataKeySettingsDirective,
dashboard: null,
aliasController: null,

22
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts

@ -39,14 +39,7 @@ import {
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import {
DataKey,
DataKeyConfigMode,
DatasourceType,
JsonSettingsSchema,
Widget,
widgetType
} from '@shared/models/widget.models';
import { DataKey, DataKeyConfigMode, DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { merge } from 'rxjs';
@ -67,6 +60,7 @@ import {
TimeSeriesChartYAxisId
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export const dataKeyValid = (key: DataKey): boolean => !!key && !!key.type && !!key.name;
@ -199,16 +193,16 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
return this.widgetConfigComponent.aliasController;
}
get dataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
get dataKeySettingsForm(): FormProperty[] {
return this.widgetConfigComponent.modelValue?.dataKeySettingsForm;
}
get dataKeySettingsDirective(): string {
return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective;
}
get latestDataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema;
get latestDataKeySettingsForm(): FormProperty[] {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsForm;
}
get latestDataKeySettingsDirective(): string {
@ -330,7 +324,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
data: {
dataKey: deepClone(this.modelValue),
dataKeyConfigMode: advanced ? DataKeyConfigMode.advanced : DataKeyConfigMode.general,
dataKeySettingsSchema: this.isLatestDataKeys ? this.latestDataKeySettingsSchema : this.dataKeySettingsSchema,
dataKeySettingsForm: this.isLatestDataKeys ? this.latestDataKeySettingsForm : this.dataKeySettingsForm,
dataKeySettingsDirective: this.isLatestDataKeys ? this.latestDataKeySettingsDirective : this.dataKeySettingsDirective,
dashboard: this.dashboard,
aliasController: this.aliasController,
@ -364,7 +358,7 @@ export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChan
}
private _generateDataKey(key: DataKey): DataKey {
key = this.callbacks.generateDataKey(key.name, key.type, this.dataKeySettingsSchema, this.isLatestDataKeys,
key = this.callbacks.generateDataKey(key.name, key.type, this.dataKeySettingsForm, this.isLatestDataKeys,
this.dataKeySettingsFunction);
if (!this.keyRowFormGroup.get('label').value) {
this.keyRowFormGroup.get('label').patchValue(key.label, {emitEvent: false});

9
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts

@ -38,7 +38,7 @@ import {
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared/models/widget.models';
import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models';
import { dataKeyRowValidator, dataKeyValid } from '@home/components/widget/config/basic/common/data-key-row.component';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -46,6 +46,7 @@ import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TimeSeriesChartYAxisId } from '@home/components/widget/lib/chart/time-series-chart.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
@ -156,8 +157,8 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
this.widgetConfigComponent.modelValue?.typeParameters?.hasAdditionalLatestDataKeys;
}
get datakeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
get dataKeySettingsForm(): FormProperty[] {
return this.widgetConfigComponent.modelValue?.dataKeySettingsForm;
}
get dataKeySettingsFunction(): DataKeySettingsFunction {
@ -281,7 +282,7 @@ export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnC
}
addKey() {
const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema,
const dataKey = this.callbacks.generateDataKey('', null, this.dataKeySettingsForm,
false, this.dataKeySettingsFunction);
dataKey.label = '';
dataKey.decimals = 0;

2
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html

@ -35,7 +35,7 @@
<div mat-dialog-content style="padding: 0; height: 800px; overflow: auto;">
<tb-data-key-config #dataKeyConfig
[dataKeyConfigMode]="dataKeyConfigMode"
[dataKeySettingsSchema]="data.dataKeySettingsSchema"
[dataKeySettingsForm]="data.dataKeySettingsForm"
[dataKeySettingsDirective]="data.dataKeySettingsDirective"
[deviceId]="data.deviceId"
[entityAliasId]="data.entityAliasId"

7
ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.ts

@ -36,11 +36,12 @@ import { IAliasController } from '@core/api/widget-api.models';
import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { TranslateService } from '@ngx-translate/core';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
export interface DataKeyConfigDialogData {
dataKey: DataKey;
dataKeyConfigMode?: DataKeyConfigMode;
dataKeySettingsSchema: any;
dataKeySettingsForm: FormProperty[];
dataKeySettingsDirective: string;
dashboard: Dashboard;
aliasController: IAliasController;
@ -92,8 +93,8 @@ export class DataKeyConfigDialogComponent extends DialogComponent<DataKeyConfigD
this.dataKeyFormGroup = this.fb.group({
dataKey: [this.data.dataKey, [Validators.required]]
});
if (this.data.dataKeySettingsSchema && this.data.dataKeySettingsSchema.schema ||
this.data.dataKeySettingsDirective && this.data.dataKeySettingsDirective.length) {
if (this.data.dataKeySettingsForm?.length ||
this.data.dataKeySettingsDirective?.length) {
this.hasAdvanced = true;
this.dataKeyConfigHeaderOptions = [
{

15
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts

@ -24,6 +24,7 @@ import {
DataKey,
dataKeyAggregationTypeHintTranslationMap,
DataKeyConfigMode,
DynamicFormData,
Widget,
widgetType
} from '@shared/models/widget.models';
@ -47,7 +48,6 @@ import { Observable, of } from 'rxjs';
import { map, mergeMap, publishReplay, refCount, tap } from 'rxjs/operators';
import { alarmFields } from '@shared/models/alarm.models';
import { JsFuncComponent } from '@shared/components/js-func.component';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
import { WidgetService } from '@core/http/widget.service';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
@ -58,6 +58,7 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models'
import { WidgetComponentService } from '@home/components/widget/widget-component.service';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { isNotEmptyTbFunction, TbFunction } from '@shared/models/js-function.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
@ -124,7 +125,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
widgetType: widgetType;
@Input()
dataKeySettingsSchema: any;
dataKeySettingsForm: FormProperty[];
@Input()
dataKeySettingsDirective: string;
@ -169,7 +170,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
public dataKeySettingsFormGroup: UntypedFormGroup;
private dataKeySettingsData: JsonFormComponentData;
private dataKeySettingsData: DynamicFormData;
private alarmKeys: Array<DataKey>;
private functionTypeKeys: Array<DataKey>;
@ -228,15 +229,11 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
type: DataKeyType.function
});
}
if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema ||
if (this.dataKeySettingsForm?.length ||
this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) {
this.hasAdvanced = true;
this.dataKeySettingsData = {
schema: this.dataKeySettingsSchema?.schema || {
type: 'object',
properties: {}
},
form: this.dataKeySettingsSchema?.form || ['*'],
settingsForm: this.dataKeySettingsForm,
settingsDirective: this.dataKeySettingsDirective
};
this.dataKeySettingsFormGroup = this.fb.group({

5
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts

@ -15,13 +15,14 @@
///
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey, JsonSettingsSchema } from '@shared/models/widget.models';
import { DataKey } from '@shared/models/widget.models';
import { Observable } from 'rxjs';
import { FormProperty } from '@shared/models/dynamic-form.models';
export type DataKeySettingsFunction = (key: DataKey, isLatestDataKey: boolean) => any;
export interface DataKeysCallbacks {
generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema,
generateDataKey: (chip: any, type: DataKeyType, dataKeySettingsForm: FormProperty[],
isLatestDataKey: boolean, dataKeySettingsFunction: DataKeySettingsFunction) => DataKey;
fetchEntityKeys: (entityAliasId: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>;
fetchEntityKeysForDevice: (deviceId: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>;

11
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts

@ -50,7 +50,7 @@ import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autoc
import { MatChipGrid, MatChipInputEvent, MatChipRow } from '@angular/material/chips';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models';
import { DataKey, DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeySettingsFunction } from './data-keys.component.models';
import { alarmFields } from '@shared/models/alarm.models';
@ -72,6 +72,7 @@ import { ColorPickerPanelComponent } from '@shared/components/color-picker/color
import { TbPopoverService } from '@shared/components/popover.service';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormProperty } from '@shared/models/dynamic-form.models';
@Component({
selector: 'tb-data-keys',
@ -151,7 +152,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
aliasController: IAliasController;
@Input()
datakeySettingsSchema: JsonSettingsSchema;
dataKeySettingsForm: FormProperty[];
@Input()
datakeySettingsFunction: DataKeySettingsFunction;
@ -375,7 +376,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
if (this.widgetType === widgetType.alarm) {
this.keys = this.utils.getDefaultAlarmDataKeys();
} else if (this.isCountDatasource) {
this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema,
this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.dataKeySettingsForm,
this.latestDataKeys, this.datakeySettingsFunction)];
} else {
this.keys = [];
@ -462,7 +463,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
}
private addFromChipValue(chip: DataKey) {
const key = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema, this.latestDataKeys,
const key = this.callbacks.generateDataKey(chip.name, chip.type, this.dataKeySettingsForm, this.latestDataKeys,
this.datakeySettingsFunction);
this.addKey(key);
}
@ -562,7 +563,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataKey: deepClone(key),
dataKeySettingsSchema: this.datakeySettingsSchema,
dataKeySettingsForm: this.dataKeySettingsForm,
dataKeySettingsDirective: this.dataKeySettingsDirective,
dashboard: this.dashboard,
aliasController: this.aliasController,

4
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html

@ -70,7 +70,7 @@
[optDataKeys]="isDataKeysOptional(datasourceFormGroup.get('type').value)"
[simpleDataKeysLabel]="!hasAdditionalLatestDataKeys"
[aliasController]="aliasController"
[datakeySettingsSchema]="dataKeySettingsSchema"
[dataKeySettingsForm]="dataKeySettingsForm"
[dataKeySettingsDirective]="dataKeySettingsDirective"
[datakeySettingsFunction]="dataKeySettingsFunction"
[dashboard]="dashboard"
@ -86,7 +86,7 @@
latestDataKeys
[optDataKeys]="true"
[aliasController]="aliasController"
[datakeySettingsSchema]="latestDataKeySettingsSchema"
[dataKeySettingsForm]="latestDataKeySettingsForm"
[dataKeySettingsDirective]="latestDataKeySettingsDirective"
[datakeySettingsFunction]="dataKeySettingsFunction"
[dashboard]="dashboard"

13
ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts

@ -29,8 +29,8 @@ import {
Datasource,
DatasourceType,
datasourceTypeTranslationMap,
JsonSettingsSchema,
Widget, WidgetConfigMode,
Widget,
WidgetConfigMode,
widgetType
} from '@shared/models/widget.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
@ -43,6 +43,7 @@ import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/wid
import { EntityType } from '@shared/models/entity-type.models';
import { DatasourcesComponent } from '@home/components/widget/config/datasources.component';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
@ -109,16 +110,16 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys;
}
public get dataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
public get dataKeySettingsForm(): FormProperty[] {
return this.widgetConfigComponent.modelValue?.dataKeySettingsForm;
}
public get dataKeySettingsDirective(): string {
return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective;
}
public get latestDataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsSchema;
public get latestDataKeySettingsForm(): FormProperty[] {
return this.widgetConfigComponent.modelValue?.latestDataKeySettingsForm;
}
public get latestDataKeySettingsDirective(): string {

13
ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts

@ -30,8 +30,8 @@ import {
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import {
Datasource,
DatasourceType, datasourceValid,
JsonSettingsSchema,
DatasourceType,
datasourceValid,
WidgetConfigMode,
widgetType
} from '@shared/models/widget.models';
@ -42,6 +42,7 @@ import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { FormProperty } from '@shared/models/dynamic-form.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
@ -340,8 +341,8 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
public addDatasource(emitEvent = true) {
let newDatasource: Datasource;
if (this.widgetConfigComponent.functionsOnly) {
newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsSchema.schema));
newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsSchema,
newDatasource = deepClone(this.utils.getDefaultDatasource(this.dataKeySettingsForm));
newDatasource.dataKeys = [this.dataKeysCallbacks.generateDataKey('Sin', DataKeyType.function, this.dataKeySettingsForm,
false, this.dataKeySettingsFunction)];
} else {
const type = this.basicMode ? this.datasourcesMode : DatasourceType.entity;
@ -355,8 +356,8 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
this.datasourcesFormArray.push(this.fb.control(newDatasource, []), {emitEvent});
}
private get dataKeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
private get dataKeySettingsForm(): FormProperty[] {
return this.widgetConfigComponent.modelValue?.dataKeySettingsForm;
}
private get dataKeySettingsFunction(): DataKeySettingsFunction {

2
ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts

@ -209,7 +209,7 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
protected constructDataKey(configData: WidgetConfigComponentData, key: DataKey, isLatestKey: boolean): DataKey {
const dataKey =
this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type,
configData.dataKeySettingsSchema, isLatestKey, configData.dataKeySettingsFunction);
configData.dataKeySettingsForm, isLatestKey, configData.dataKeySettingsFunction);
if (key.label) {
dataKey.label = key.label;
}

5
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html

@ -18,7 +18,8 @@
<div [formGroup]="widgetSettingsFormGroup">
<ng-container #definedSettingsContent></ng-container>
<div class="tb-settings-directive-error" *ngIf="definedDirectiveError">{{definedDirectiveError}}</div>
<tb-json-form *ngIf="useJsonForm()" #jsonFormComponent
<tb-dynamic-form *ngIf="useDynamicForm()"
[properties]="settingsForm"
formControlName="settings">
</tb-json-form>
</tb-dynamic-form>
</div>

24
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts

@ -38,15 +38,14 @@ import {
} from '@angular/forms';
import { Subscription } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { JsonFormComponent } from '@shared/components/json-form/json-form.component';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
import { IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models';
import { DynamicFormData, IWidgetSettingsComponent, Widget, WidgetSettings } from '@shared/models/widget.models';
import { widgetSettingsComponentsMap } from '@home/components/widget/lib/settings/widget-settings.module';
import { Dashboard } from '@shared/models/dashboard.models';
import { WidgetService } from '@core/http/widget.service';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
@Component({
selector: 'tb-widget-settings',
@ -67,8 +66,6 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
@ViewChild('definedSettingsContent', {read: ViewContainerRef, static: true}) definedSettingsContainer: ViewContainerRef;
@ViewChild('jsonFormComponent') jsonFormComponent: JsonFormComponent;
@Input()
disabled: boolean;
@ -91,6 +88,8 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
definedDirectiveError: string;
settingsForm?: FormProperty[];
widgetSettingsFormGroup: UntypedFormGroup;
changeSubscription: Subscription;
@ -98,7 +97,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
private definedSettingsComponentRef: ComponentRef<IWidgetSettingsComponent>;
private definedSettingsComponent: IWidgetSettingsComponent;
private widgetSettingsFormData: JsonFormComponentData;
private widgetSettingsFormData: DynamicFormData;
private propagateChange = (_v: any) => { };
constructor(private translate: TranslateService,
@ -169,8 +168,9 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
}
}
writeValue(value: JsonFormComponentData): void {
writeValue(value: DynamicFormData): void {
this.widgetSettingsFormData = value;
this.settingsForm = this.widgetSettingsFormData.settingsForm;
if (this.changeSubscription) {
this.changeSubscription.unsubscribe();
this.changeSubscription = null;
@ -185,10 +185,10 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
this.updateModel(settings);
});
} else {
this.widgetSettingsFormGroup.get('settings').patchValue(this.widgetSettingsFormData, {emitEvent: false});
this.widgetSettingsFormGroup.get('settings').patchValue(this.widgetSettingsFormData.model, {emitEvent: false});
this.changeSubscription = this.widgetSettingsFormGroup.get('settings').valueChanges.subscribe(
(widgetSettingsFormData: JsonFormComponentData) => {
this.updateModel(widgetSettingsFormData.model);
(data: WidgetSettings) => {
this.updateModel(data);
}
);
}
@ -199,7 +199,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
this.settingsDirective.length && !this.definedDirectiveError;
}
useJsonForm(): boolean {
useDynamicForm(): boolean {
return !this.settingsDirective || !this.settingsDirective.length;
}
@ -250,7 +250,7 @@ export class WidgetSettingsComponent implements ControlValueAccessor, OnDestroy,
}
};
}
} else if (this.useJsonForm()) {
} else if (this.useDynamicForm()) {
if (!this.widgetSettingsFormGroup.get('settings').valid) {
return {
widgetSettings: {

13
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -19,23 +19,18 @@ import LeafletMap from './leaflet-map';
import { MapWidgetInterface, MapWidgetStaticInterface } from './map-widget.interface';
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import { getDefCenterPosition, parseWithTranslation } from './common-maps-utils';
import {
Datasource,
DatasourceData,
FormattedData,
JsonSettingsSchema,
WidgetActionDescriptor
} from '@shared/models/widget.models';
import { Datasource, DatasourceData, FormattedData, WidgetActionDescriptor } from '@shared/models/widget.models';
import { TranslateService } from '@ngx-translate/core';
import { UtilsService } from '@core/services/utils.service';
import { EntityDataPageLink } from '@shared/models/query/query.models';
import { providerClass } from '@home/components/widget/lib/maps/providers/public-api';
import { isDefined, isDefinedAndNotNull, parseFunction, parseTbFunction } from '@core/utils';
import { isDefined, isDefinedAndNotNull, parseTbFunction } from '@core/utils';
import L from 'leaflet';
import { firstValueFrom, forkJoin, from, Observable, of } from 'rxjs';
import { AttributeService } from '@core/http/attribute.service';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
// @dynamic
export class MapWidgetController implements MapWidgetInterface {
@ -113,7 +108,7 @@ export class MapWidgetController implements MapWidgetInterface {
map: LeafletMap;
provider: MapProviders;
schema: JsonSettingsSchema;
form: FormProperty[];
data: DatasourceData[];
settings: WidgetUnitedMapSettings;
pageLink: EntityDataPageLink;

182
ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts

@ -65,6 +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,
defaultPropertyValue,
FormProperty,
FormPropertyType
} from '@shared/models/dynamic-form.models';
export interface ScadaSymbolApi {
generateElementId: () => string;
@ -151,60 +157,6 @@ export interface ScadaSymbolBehaviorAction extends ScadaSymbolBehaviorBase {
export type ScadaSymbolBehavior = ScadaSymbolBehaviorValue & ScadaSymbolBehaviorAction;
export enum ScadaSymbolPropertyType {
text = 'text',
number = 'number',
switch = 'switch',
color = 'color',
color_settings = 'color_settings',
font = 'font',
units = 'units',
icon = 'icon'
}
export const scadaSymbolPropertyTypes = Object.keys(ScadaSymbolPropertyType) as ScadaSymbolPropertyType[];
export const scadaSymbolPropertyTypeTranslations = new Map<ScadaSymbolPropertyType, string>(
[
[ScadaSymbolPropertyType.text, 'scada.property.type-text'],
[ScadaSymbolPropertyType.number, 'scada.property.type-number'],
[ScadaSymbolPropertyType.switch, 'scada.property.type-switch'],
[ScadaSymbolPropertyType.color, 'scada.property.type-color'],
[ScadaSymbolPropertyType.color_settings, 'scada.property.type-color-settings'],
[ScadaSymbolPropertyType.font, 'scada.property.type-font'],
[ScadaSymbolPropertyType.units, 'scada.property.type-units'],
[ScadaSymbolPropertyType.icon, 'scada.property.type-icon']
]
);
export const scadaSymbolPropertyRowClasses =
['column', 'column-xs', 'column-lt-md', 'align-start', 'no-border', 'no-gap', 'no-padding', 'same-padding'];
export const scadaSymbolPropertyFieldClasses =
['medium-width', 'flex', 'flex-xs', 'flex-lt-md'];
export interface ScadaSymbolPropertyBase {
id: string;
name: string;
type: ScadaSymbolPropertyType;
default: any;
required?: boolean;
subLabel?: string;
divider?: boolean;
fieldSuffix?: string;
disableOnProperty?: string;
rowClass?: string;
fieldClass?: string;
}
export interface ScadaSymbolNumberProperty extends ScadaSymbolPropertyBase {
min?: number;
max?: number;
step?: number;
}
export type ScadaSymbolProperty = ScadaSymbolPropertyBase & ScadaSymbolNumberProperty;
export interface ScadaSymbolMetadata {
title: string;
description?: string;
@ -215,7 +167,7 @@ export interface ScadaSymbolMetadata {
stateRender?: ScadaSymbolStateRenderFunction;
tags: ScadaSymbolTag[];
behavior: ScadaSymbolBehavior[];
properties: ScadaSymbolProperty[];
properties: FormProperty[];
}
export const emptyMetadata = (width?: number, height?: number): ScadaSymbolMetadata => ({
@ -507,7 +459,7 @@ export const defaultScadaSymbolObjectSettings = (metadata: ScadaSymbolMetadata):
}
}
for (const property of metadata.properties) {
settings.properties[property.id] = property.default;
settings.properties[property.id] = defaultFormPropertyValue(property);
}
return settings;
};
@ -683,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) {
@ -1020,39 +974,107 @@ export class ScadaSymbolObject {
return Array.isArray(element) ? element : [element];
}
private getProperty(id: string): ScadaSymbolProperty {
return this.metadata.properties.find(p => p.id === id);
private getProperty(properties: FormProperty[], ...ids: string[]): FormProperty {
let found: FormProperty;
for (const id of ids) {
if (properties) {
found = properties.find(p => p.id === id);
if (found && found.type === FormPropertyType.fieldset) {
properties = found.properties;
} else {
properties = null;
}
} else {
found = null;
}
}
return found;
}
private getPropertyValue(id: string): any {
const property = this.getProperty(id);
if (property) {
const value = this.settings.properties[id];
if (isDefinedAndNotNull(value)) {
if (property.type === ScadaSymbolPropertyType.color_settings) {
return ColorProcessor.fromSettings(value);
} else if (property.type === ScadaSymbolPropertyType.text) {
const result = this.ctx.utilsService.customTranslation(value, value);
const entityInfo = this.ctx.defaultSubscription.getFirstEntityInfo();
return createLabelFromSubscriptionEntityInfo(entityInfo, result);
private getSettingsValue(settings: {[id: string]: any}, ...ids: string[]): any {
let found: any;
let properties = settings;
for (const id of ids) {
if (properties) {
found = properties[id];
if (found && typeof found === 'object') {
properties = found;
} else {
properties = null;
}
return value;
} else {
switch (property.type) {
case ScadaSymbolPropertyType.text:
return '';
case ScadaSymbolPropertyType.number:
return 0;
case ScadaSymbolPropertyType.color:
return '#000';
case ScadaSymbolPropertyType.color_settings:
return ColorProcessor.fromSettings(constantColor('#000'));
found = null;
}
}
return found;
}
private getPropertyValue(properties: FormProperty[], settings: {[id: string]: any}, ...ids: string[]): any {
const property = this.getProperty(properties, ...ids);
if (property) {
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) {
if (childProperty.type !== FormPropertyType.htmlSection) {
propertyValue[childProperty.id] = this.getPropertyValue(properties, settings, ...ids, childProperty.id);
}
}
return propertyValue;
} else {
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';

74
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html

@ -0,0 +1,74 @@
<!--
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.
-->
<div class="tb-dynamic-form-array tb-form-panel stroked" [class.no-padding-right]="!disabled || !propertiesFormArray().controls.length">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<div class="tb-form-panel-title">{{ title }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div *ngIf="propertiesFormArray().controls.length; else noProperties" class="tb-dynamic-form-array-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="propertyDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
class="tb-draggable tb-dynamic-form-array-row"
*ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;">
<div class="tb-dynamic-form-item-index-container">
<div class="tb-dynamic-form-item-index">{{$index + 1}}</div>
</div>
<tb-dynamic-form class="flex-1"
[properties]="[itemProperty]"
[formControl]="propertyControl"
isArrayItem noPadding noBorder>
</tb-dynamic-form>
<div class="tb-dynamic-form-array-buttons">
<button mat-icon-button
*ngIf="!disabled"
type="button"
(click)="removeProperty($index)"
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button
*ngIf="!disabled"
type="button"
cdkDragHandle
[class.tb-hidden]="!dragEnabled"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
</div>
<div *ngIf="!disabled || !propertiesFormArray().controls.length" [class.invisible]="disabled">
<button type="button" mat-stroked-button color="primary" (click)="addProperty()">
<mat-icon>add</mat-icon>
{{ 'action.add' | translate }}
</button>
</div>
</ng-template>
</mat-expansion-panel>
</div>
<ng-template #noProperties>
<span class="tb-prompt flex items-center justify-center">{{ 'dynamic-form.property.no-items' | translate }}</span>
</ng-template>

72
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;
}
}
}
}

161
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<string[]>) {
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<AbstractControl> = [];
if (values) {
values.forEach((value) => {
const property = {
[this.itemProperty.id]: value
};
propertiesControls.push(this.fb.control(property, []));
});
}
return this.fb.array(propertiesControls);
}
}

93
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html

@ -0,0 +1,93 @@
<!--
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.
-->
<div class="tb-form-panel tb-dynamic-form-properties" [class.no-border]="noBorder" [class.no-padding]="noBorder" [class.no-margin]="noMargin">
<div class="tb-form-table" [class.flex-1]="fillHeight">
<div class="tb-form-table-header" [class.no-padding-right]="!disabled">
<div class="tb-form-table-header-cell tb-id-header" translate>dynamic-form.property.id</div>
<div class="tb-form-table-header-cell tb-name-header" translate>dynamic-form.property.name</div>
<div class="tb-form-table-header-cell tb-type-header" translate>dynamic-form.property.type</div>
<div class="tb-form-table-header-cell tb-actions-header tb-form-table-row-cell-buttons justify-end" [class.disabled]="disabled">
<button mat-icon-button
*ngIf="!disabled && importExport"
type="button"
matTooltip="{{ 'dynamic-form.import-form' | translate }}"
matTooltipPosition="above"
(click)="import($event)">
<tb-icon>mdi:file-import</tb-icon>
</button>
<button mat-icon-button
*ngIf="!disabled && importExport"
[class.tb-hidden]="!propertiesFormArray().controls.length"
type="button"
[disabled]="!propertiesFormArray().controls.length"
matTooltip="{{ 'dynamic-form.export-form' | translate }}"
matTooltipPosition="above"
(click)="export($event)">
<tb-icon>mdi:file-export</tb-icon>
</button>
<button mat-icon-button
*ngIf="!disabled"
[class.tb-hidden]="!propertiesFormArray().controls.length"
type="button"
[disabled]="!propertiesFormArray().controls.length"
matTooltip="{{ 'dynamic-form.clear-form' | translate }}"
matTooltipPosition="above"
(click)="clear($event)">
<tb-icon>mdi:broom</tb-icon>
</button>
</div>
</div>
<div *ngIf="propertiesFormArray().controls.length; else noProperties" class="tb-form-table-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="propertyDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
[class.tb-draggable]="!disabled"
[class.tb-draggable-form-table-row]="!disabled"
*ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;">
<tb-dynamic-form-property-row class="flex-1"
[index]="$index"
[booleanPropertyIds]="booleanPropertyIds"
[formControl]="propertyControl"
(propertyRemoved)="removeProperty($index)">
</tb-dynamic-form-property-row>
<div class="tb-form-table-row-cell-buttons">
<button mat-icon-button
*ngIf="!disabled"
type="button"
cdkDragHandle
[class.tb-hidden]="!dragEnabled"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
</div>
<tb-error *ngIf="errorText" noMargin [error]="errorText" style="padding-left: 12px;"></tb-error>
</div>
<div *ngIf="!disabled">
<button type="button" mat-stroked-button color="primary" (click)="addProperty()">
{{ 'dynamic-form.property.add-property' | translate }}
</button>
</div>
</div>
<ng-template #noProperties>
<span class="tb-prompt flex items-center justify-center" [style]="fillHeight ? {height: '100%'} : {}">{{ 'dynamic-form.property.no-properties' | translate }}</span>
</ng-template>

8
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.scss

@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-scada-symbol-properties {
.tb-dynamic-form-properties {
flex: 1;
margin: 12px;
&:not(.no-margin) {
margin: 12px;
}
.tb-form-table-header-cell {
&.tb-id-header {
flex: 1 1 40%;
@ -40,7 +42,7 @@
}
.tb-form-table-body {
overflow: auto;
tb-scada-symbol-metadata-property-row {
tb-dynamic-form-property-row {
overflow: hidden;
}
}

129
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts

@ -36,44 +36,74 @@ import {
UntypedFormGroup,
Validator
} from '@angular/forms';
import { ScadaSymbolProperty, ScadaSymbolPropertyType } from '@home/components/widget/lib/scada/scada-symbol.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';
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';
import { coerceBoolean } from '@shared/decorators/coercion';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { DialogService } from '@core/services/dialog.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-scada-symbol-metadata-properties',
templateUrl: './scada-symbol-properties.component.html',
styleUrls: ['./scada-symbol-properties.component.scss'],
selector: 'tb-dynamic-form-properties',
templateUrl: './dynamic-form-properties.component.html',
styleUrls: ['./dynamic-form-properties.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaSymbolPropertiesComponent),
useExisting: forwardRef(() => DynamicFormPropertiesComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ScadaSymbolPropertiesComponent),
useExisting: forwardRef(() => DynamicFormPropertiesComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnInit, Validator {
export class DynamicFormPropertiesComponent implements ControlValueAccessor, OnInit, Validator {
@HostBinding('style.display') styleDisplay = 'flex';
@HostBinding('style.overflow') styleOverflow = 'hidden';
@HostBinding('style.height')
get containerHeight(): string {
return this.fillHeight ? '100%': 'auto';
}
@ViewChildren(ScadaSymbolPropertyRowComponent)
propertyRows: QueryList<ScadaSymbolPropertyRowComponent>;
@ViewChildren(DynamicFormPropertyRowComponent)
propertyRows: QueryList<DynamicFormPropertyRowComponent>;
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
noBorder = false;
@Input()
@coerceBoolean()
noMargin = false;
@Input()
@coerceBoolean()
fillHeight = false;
@Input()
@coerceBoolean()
importExport = false;
@Input()
exportFileName = 'form';
booleanPropertyIds: string[] = [];
propertiesFormGroup: UntypedFormGroup;
@ -88,6 +118,8 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
constructor(private fb: UntypedFormBuilder,
private translate: TranslateService,
private importExportService: ImportExportService,
private dialogService: DialogService,
private destroyRef: DestroyRef) {
}
@ -99,11 +131,8 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => {
let properties: ScadaSymbolProperty[] = this.propertiesFormGroup.get('properties').value;
if (properties) {
properties = properties.filter(p => propertyValid(p));
}
this.booleanPropertyIds = properties.filter(p => p.type === ScadaSymbolPropertyType.switch).map(p => p.id);
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)) {
p.disableOnProperty = null;
@ -111,7 +140,7 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
controls[i].patchValue(p, {emitEvent: false});
}
});
this.propagateChange(properties);
this.propagateChange(cleanupFormProperties(properties));
}
);
}
@ -120,7 +149,7 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
@ -132,13 +161,13 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
}
}
writeValue(value: ScadaSymbolProperty[] | undefined): void {
writeValue(value: FormProperty[] | undefined): void {
const properties= value || [];
this.propertiesFormGroup.setControl('properties', this.preparePropertiesFormArray(properties), {emitEvent: false});
this.booleanPropertyIds = properties.filter(p => p.type === ScadaSymbolPropertyType.switch).map(p => p.id);
this.booleanPropertyIds = properties.filter(p => 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 =
@ -146,7 +175,7 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
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');
this.errorText = this.translate.instant('dynamic-form.property.not-unique-property-ids-error');
}
}
const valid = this.propertiesFormGroup.valid;
@ -173,15 +202,15 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
propertyDrop(event: CdkDragDrop<string[]>) {
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;
}
@ -190,10 +219,10 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
}
addProperty() {
const property: ScadaSymbolProperty = {
const property: FormProperty = {
id: '',
name: '',
type: ScadaSymbolPropertyType.text,
type: FormPropertyType.text,
default: ''
};
const propertiesArray = this.propertiesFormGroup.get('properties') as UntypedFormArray;
@ -207,7 +236,39 @@ export class ScadaSymbolPropertiesComponent implements ControlValueAccessor, OnI
});
}
private preparePropertiesFormArray(properties: ScadaSymbolProperty[] | undefined): UntypedFormArray {
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<AbstractControl> = [];
if (properties) {
properties.forEach((property) => {
@ -216,4 +277,12 @@ export class ScadaSymbolPropertiesComponent 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;
}
}

471
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html

@ -0,0 +1,471 @@
<!--
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.
-->
<div class="tb-dynamic-form-property-settings-panel" [formGroup]="propertyFormGroup">
<div class="tb-dynamic-form-property-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-dynamic-form-property-settings-panel-content">
<div class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.id</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.name</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.group-title</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="group" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.type</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of formPropertyTypes" [value]="type">
{{ formPropertyTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="isArray" class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dynamic-form.property.array-item</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.item-type</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="arrayItemType">
<mat-option *ngFor="let type of arrayItemFormPropertyTypes" [value]="type">
{{ formPropertyTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.item-name</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="arrayItemName" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<ng-container *ngTemplateOutlet="itemSettings">
</ng-container>
</div>
<ng-container *ngIf="!isArray">
<ng-container *ngTemplateOutlet="itemSettings">
</ng-container>
</ng-container>
<div *ngIf="![FormPropertyType.fieldset, FormPropertyType.htmlSection, FormPropertyType.array].includes(propertyFormGroup.get('type').value)" class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="required">
{{ 'dynamic-form.property.value-required' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
{{ 'dynamic-form.property.advanced-ui-settings' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<ng-container *ngIf="isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value)">
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.sub-label</div>
<mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="subLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="divider">
{{ 'dynamic-form.property.vertical-divider-after' | translate }}
</mat-slide-toggle>
</div>
</ng-container>
<div *ngIf="[FormPropertyType.number, FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)"
class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.input-field-suffix</div>
<mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="fieldSuffix" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="propertyFormGroup.get('type').value !== FormPropertyType.htmlSection" class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.disable-on-property</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="disableOnProperty" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="null"></mat-option>
<mat-option *ngFor="let prop of booleanPropertyIds" [value]="prop">
{{ prop }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row">
<tb-js-func
class="flex-1"
formControlName="condition"
[functionArgs]="['property', 'model']"
functionTitle="{{ 'dynamic-form.property.display-condition-function' | translate }}">
</tb-js-func>
</div>
<tb-string-items-list *ngIf="isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value)"
class="flex-1"
fieldClass="flex"
floatLabel="always"
label="{{ 'dynamic-form.property.property-row-classes' | translate }}"
placeholder="{{ 'widget-config.set' | translate }}"
[predefinedValues]="formPropertyRowClasses"
formControlName="rowClass"
appearance="outline"
subscriptSizing="dynamic">
</tb-string-items-list>
<tb-string-items-list *ngIf="isPropertyTypeAllowedForRow(propertyFormGroup.get('type').value) && FormPropertyType.switch !== propertyFormGroup.get('type').value"
class="flex-1"
fieldClass="flex"
floatLabel="always"
label="{{ 'dynamic-form.property.property-field-classes' | translate }}"
placeholder="{{ 'widget-config.set' | translate }}"
[predefinedValues]="formPropertyFieldClasses"
formControlName="fieldClass"
appearance="outline"
subscriptSizing="dynamic">
</tb-string-items-list>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<div class="tb-dynamic-form-property-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ (disabled ? 'action.close' : 'action.cancel') | translate }}
</button>
<button *ngIf="!disabled"
mat-raised-button
color="primary"
type="button"
(click)="applyPropertySettings()"
[disabled]="propertyFormGroup.invalid || !propertyFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.apply') | translate }}
</button>
</div>
</div>
<ng-template #itemSettings>
<ng-container [formGroup]="propertyFormGroup">
<div *ngIf="propertyItemType === FormPropertyType.textarea" class="tb-form-row space-between">
<div translate>dynamic-form.property.textarea-rows</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="rows" min="1" step="1"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="propertyItemType === FormPropertyType.number" class="tb-form-row space-between">
<div translate>dynamic-form.property.number-settings</div>
<div class="flex flex-1 flex-row items-center justify-end gap-2">
<div class="tb-small-label" translate>dynamic-form.property.min</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="min"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>dynamic-form.property.max</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="max"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>dynamic-form.property.step</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="step" min="0"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div *ngIf="propertyItemType === FormPropertyType.fieldset" class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
{{ 'dynamic-form.property.properties' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-dynamic-form-properties formControlName="properties" noBorder noMargin>
</tb-dynamic-form-properties>
</ng-template>
</mat-expansion-panel>
</div>
<div *ngIf="propertyItemType === FormPropertyType.htmlSection" class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
{{ 'dynamic-form.property.html-section-settings' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row">
<tb-html
class="flex-1"
formControlName="htmlContent"
label="{{ 'dynamic-form.property.html-section-content' | translate }}">
</tb-html>
</div>
<tb-string-items-list class="flex-1"
fieldClass="flex"
floatLabel="always"
label="{{ 'dynamic-form.property.html-section-classes' | translate }}"
formControlName="htmlClassList"
appearance="outline"
subscriptSizing="dynamic">
</tb-string-items-list>
</ng-template>
</mat-expansion-panel>
</div>
<ng-container *ngIf="[FormPropertyType.select, FormPropertyType.radios].includes(propertyItemType)">
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
{{ (propertyFormGroup.get('type').value === FormPropertyType.select ? 'dynamic-form.property.select-options' : 'dynamic-form.property.radio-button-options') | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div *ngIf="propertyItemType === FormPropertyType.radios" class="tb-form-row space-between">
<div translate>dynamic-form.property.buttons-direction</div>
<mat-chip-listbox class="center-stretch" formControlName="direction">
<mat-chip-option [selectable]="propertyFormGroup.get('direction').value !== 'column'"
value="column">{{ 'dynamic-form.property.direction-column' | translate }}</mat-chip-option>
<mat-chip-option [selectable]="propertyFormGroup.get('direction').value !== 'row'"
value="row">{{ 'dynamic-form.property.direction-row' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div *ngIf="propertyItemType === FormPropertyType.select" class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="multiple">
{{ 'dynamic-form.property.enable-multiple-select' | translate }}
</mat-slide-toggle>
</div>
<div *ngIf="propertyItemType === FormPropertyType.select && !propertyFormGroup.get('multiple').value" class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="allowEmptyOption">
{{ 'dynamic-form.property.allow-empty-select-option' | translate }}
</mat-slide-toggle>
</div>
<div *ngIf="propertyItemType === FormPropertyType.select && propertyFormGroup.get('multiple').value" class="tb-form-row space-between">
<div translate>dynamic-form.property.selected-options-limit</div>
<div class="flex flex-1 flex-row items-center justify-end gap-2">
<div class="tb-small-label" translate>dynamic-form.property.min</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="minItems"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>dynamic-form.property.max</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="maxItems"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<tb-dynamic-form-select-items
formControlName="items">
</tb-dynamic-form-select-items>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container>
<div *ngIf="[FormPropertyType.javascript, FormPropertyType.markdown].includes(propertyItemType)" class="tb-form-row">
<div class="fixed-title-width" translate>dynamic-form.property.help-id</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="helpId" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<ng-container *ngIf="propertyItemType === FormPropertyType.datetime">
<div class="tb-form-row space-between">
<div translate>dynamic-form.property.datetime-type</div>
<mat-chip-listbox class="center-stretch" formControlName="dateTimeType">
<mat-chip-option [selectable]="propertyFormGroup.get('dateTimeType').value !== 'date'"
value="date">{{ 'dynamic-form.property.datetime-type-date' | translate }}</mat-chip-option>
<mat-chip-option [selectable]="propertyFormGroup.get('dateTimeType').value !== 'time'"
value="time">{{ 'dynamic-form.property.datetime-type-time' | translate }}</mat-chip-option>
<mat-chip-option [selectable]="propertyFormGroup.get('dateTimeType').value !== 'datetime'"
value="datetime">{{ 'dynamic-form.property.datetime-type-datetime' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="allowClear">
{{ 'dynamic-form.property.enable-clear-button' | translate }}
</mat-slide-toggle>
</div>
</ng-container>
<div *ngIf="propertyItemType === FormPropertyType.textarea" class="tb-form-row no-border no-padding">
<mat-form-field class="flex"
floatLabel="always"
appearance="outline" subscriptSizing="dynamic">
<mat-label translate>dynamic-form.property.default-value</mat-label>
<textarea matInput
[rows]="propertyFormGroup.get('rows').value || 2"
formControlName="default" placeholder="{{ 'widget-config.set' | translate }}"></textarea>
</mat-form-field>
</div>
<div *ngIf="propertyItemType === FormPropertyType.javascript" class="tb-form-row" >
<tb-js-func
class="flex-1"
disableUndefinedCheck
formControlName="default"
[helpId]="propertyFormGroup.get('helpId').value"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-js-func>
</div>
<div *ngIf="propertyItemType === FormPropertyType.json" class="tb-form-row" >
<tb-json-content
class="flex-1"
formControlName="default"
[contentType]="ContentType.JSON"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-json-content>
</div>
<div *ngIf="propertyItemType === FormPropertyType.html" class="tb-form-row" >
<tb-html
class="flex-1"
formControlName="default"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-html>
</div>
<div *ngIf="propertyItemType === FormPropertyType.css" class="tb-form-row" >
<tb-css
class="flex-1"
formControlName="default"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-css>
</div>
<div *ngIf="propertyItemType === FormPropertyType.markdown" class="tb-form-row" >
<tb-markdown-editor
class="flex-1"
formControlName="default"
[helpId]="propertyFormGroup.get('helpId').value"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-markdown-editor>
</div>
<div *ngIf="propertyItemType === FormPropertyType.image && !isArray" class="tb-form-row" >
<tb-gallery-image-input
class="flex-1"
formControlName="default"
label="{{ 'dynamic-form.property.default-value' | translate }}">
</tb-gallery-image-input>
</div>
<div *ngIf="propertyItemType === FormPropertyType.radios"
class="tb-form-row tb-radios-property" [class.direction-column]="propertyFormGroup.get('direction').value === 'column'">
<div tbTruncateWithTooltip translate class="fixed-title-height">dynamic-form.property.default-value</div>
<mat-radio-group class="flex flex-1 flex-wrap" [class.flex-col]="propertyFormGroup.get('direction').value === 'column'"
formControlName="default">
<mat-radio-button *ngFor="let item of propertyFormGroup.get('items').value" [value]="item.value">
<div tbTruncateWithTooltip>{{ item.label | customTranslate }}</div>
</mat-radio-button>
</mat-radio-group>
</div>
<div *ngIf="isPropertyTypeAllowedForRow(propertyItemType)" class="tb-form-row space-between">
<div class="fixed-title-width" translate>dynamic-form.property.default-value</div>
<mat-form-field *ngIf="[FormPropertyType.text, FormPropertyType.password].includes(propertyItemType)"
class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput [type]="propertyFormGroup.get('type').value" formControlName="default" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input *ngIf="propertyItemType === FormPropertyType.color"
asBoxInput
colorClearButton
formControlName="default">
</tb-color-input>
<tb-color-settings *ngIf="propertyItemType === FormPropertyType.color_settings"
formControlName="default"
settingsKey="{{ propertyFormGroup.get('id').value }}">
</tb-color-settings>
<mat-form-field *ngIf="propertyItemType === FormPropertyType.number"
appearance="outline"
class="number"
subscriptSizing="dynamic">
<input matInput formControlName="default"
[min]="propertyFormGroup.get('min').value"
[max]="propertyFormGroup.get('max').value"
[step]="propertyFormGroup.get('step').value"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings *ngIf="propertyItemType === FormPropertyType.font"
formControlName="default"
clearButton
disabledLineHeight>
</tb-font-settings>
<tb-unit-input *ngIf="propertyItemType === FormPropertyType.units"
formControlName="default"></tb-unit-input>
<tb-value-input *ngIf="propertyItemType === FormPropertyType.switch"
[valueType]="ValueType.BOOLEAN"
formControlName="default"></tb-value-input>
<tb-material-icon-select *ngIf="propertyItemType === FormPropertyType.icon"
asBoxInput
formControlName="default">
</tb-material-icon-select>
<mat-form-field *ngIf="propertyItemType === FormPropertyType.select" class="medium-width"
appearance="outline" subscriptSizing="dynamic">
<mat-select *ngIf="!propertyFormGroup.get('multiple').value" formControlName="default">
<mat-option *ngIf="propertyFormGroup.get('allowEmptyOption').value" [value]="null"></mat-option>
<mat-option *ngFor="let item of propertyFormGroup.get('items').value" [value]="item.value">
{{ item.label | customTranslate }}
</mat-option>
</mat-select>
<mat-select *ngIf="propertyFormGroup.get('multiple').value" formControlName="default" multiple>
<mat-option *ngFor="let item of propertyFormGroup.get('items').value" [value]="item.value">
{{ item.label | customTranslate }}
</mat-option>
</mat-select>
</mat-form-field>
<ng-container *ngIf="propertyItemType === FormPropertyType.datetime">
<tb-datetime *ngIf="propertyFormGroup.get('dateTimeType').value === 'date'"
class="flex-1"
showLabel="false"
fieldClass="flex"
[allowClear]="propertyFormGroup.get('allowClear').value"
appearance="outline"
subscriptSizing="dynamic"
type="date"
formControlName="default"
></tb-datetime>
<tb-datetime *ngIf="propertyFormGroup.get('dateTimeType').value === 'time'"
class="flex-1"
showLabel="false"
fieldClass="flex"
[allowClear]="propertyFormGroup.get('allowClear').value"
appearance="outline"
subscriptSizing="dynamic"
type="time"
formControlName="default"
></tb-datetime>
<tb-datetime *ngIf="propertyFormGroup.get('dateTimeType').value === 'datetime'"
class="flex-1"
showLabel="false"
fieldClass="flex"
[allowClear]="propertyFormGroup.get('allowClear').value"
appearance="outline"
subscriptSizing="dynamic"
type="datetime"
formControlName="default"
></tb-datetime>
</ng-container>
</div>
</ng-container>
</ng-template>

31
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../scss/constants';
@import '../../../../../../../../../scss/constants';
.tb-scada-symbol-property-settings-panel {
.tb-dynamic-form-property-settings-panel {
width: 540px;
display: flex;
flex-direction: column;
@ -23,7 +23,7 @@
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-scada-symbol-property-settings-panel-content {
.tb-dynamic-form-property-settings-panel-content {
display: flex;
flex-direction: column;
gap: 16px;
@ -31,14 +31,14 @@
margin: -10px;
padding: 10px;
}
.tb-scada-symbol-property-settings-title {
.tb-dynamic-form-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 {
.tb-dynamic-form-property-settings-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
@ -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;
}
}
}
}
}
}

317
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts

@ -0,0 +1,317 @@
///
/// 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, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ContentType, ValueType } from '@shared/models/constants';
import {
defaultPropertyValue,
FormProperty,
formPropertyFieldClasses,
formPropertyRowClasses,
FormPropertyType,
formPropertyTypes,
formPropertyTypeTranslations,
FormSelectItem,
isPropertyTypeAllowedForRow
} from '@shared/models/dynamic-form.models';
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',
templateUrl: './dynamic-form-property-panel.component.html',
styleUrls: ['./dynamic-form-property-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class DynamicFormPropertyPanelComponent implements OnInit {
ValueType = ValueType;
FormPropertyType = FormPropertyType;
ContentType = ContentType;
formPropertyTypes = formPropertyTypes;
arrayItemFormPropertyTypes = formPropertyTypes.filter(t => t !== FormPropertyType.array);
formPropertyTypeTranslations = formPropertyTypeTranslations;
formPropertyRowClasses: StringItemsOption[] = formPropertyRowClasses.map(clazz => ({name: clazz, value: clazz}));
formPropertyFieldClasses: StringItemsOption[] = formPropertyFieldClasses.map(clazz => ({name: clazz, value: clazz}));
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;
@Input()
property: FormProperty;
@Input()
booleanPropertyIds: string[];
@Input()
disabled: boolean;
@Input()
popover: TbPopoverComponent<DynamicFormPropertyPanelComponent>;
@Output()
propertySettingsApplied = new EventEmitter<FormProperty>();
panelTitle: string;
propertyFormGroup: UntypedFormGroup;
private propertyType: FormPropertyType;
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.panelTitle = this.isAdd ? 'dynamic-form.property.add-property' : 'dynamic-form.property.property-settings';
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, []],
divider: [this.property.divider, []],
fieldSuffix: [this.property.fieldSuffix, []],
disableOnProperty: [this.property.disableOnProperty, []],
condition: [this.property.condition, []],
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, []],
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', []],
allowClear: [this.property.allowClear || true, []],
dateTimeType: [this.property.dateTimeType || 'datetime', []],
htmlContent: [this.property.htmlContent || '', []],
htmlClassList: [this.property.htmlClassList || [], []]
}
);
if (this.disabled) {
this.propertyFormGroup.disable({emitEvent: false});
} else {
this.propertyFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).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(() => {
this.onSelectItemsChange();
});
this.propertyFormGroup.get('multiple').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.onMultipleSelectChange();
});
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() {
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});
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 (type === FormPropertyType.fieldset) {
this.propertyFormGroup.get('default').disable({emitEvent: false});
this.propertyFormGroup.get('properties').enable({emitEvent: false});
} else {
this.propertyFormGroup.get('default').enable({emitEvent: false});
this.propertyFormGroup.get('properties').disable({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});
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) {
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 = 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 && [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});
} else {
if (!items.find(item => item.value === defaultValue)) {
this.propertyFormGroup.get('default').patchValue(null, {emitEvent: false});
}
}
}
}
private onMultipleSelectChange() {
const type = this.propertyItemType;
const multiple: boolean = this.propertyFormGroup.get('multiple').value;
const defaultValue: any = this.propertyFormGroup.get('default').value;
if (type === FormPropertyType.select) {
if (multiple) {
if (defaultValue && !Array.isArray(defaultValue)) {
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})
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;
setTimeout(() => {
this.propertyFormGroup.get('default').patchValue(newVal, {emitEvent: false});
});
}
this.propertyFormGroup.get('allowEmptyOption').enable({emitEvent: false});
this.propertyFormGroup.get('minItems').disable({emitEvent: false});
this.propertyFormGroup.get('maxItems').disable({emitEvent: false});
}
}
}
}

10
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div [formGroup]="propertyRowFormGroup" class="tb-form-table-row tb-scada-symbol-metadata-property-row">
<div [formGroup]="propertyRowFormGroup" class="tb-form-table-row tb-dynamic-form-property-row">
<mat-form-field class="tb-inline-field tb-id-field" appearance="outline" subscriptSizing="dynamic">
<input #idInput required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
@ -24,8 +24,8 @@
</mat-form-field>
<mat-form-field class="tb-inline-field tb-type-field fixed-height" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of scadaSymbolPropertyTypes" [value]="type">
{{ scadaSymbolPropertyTypeTranslations.get(type) | translate }}
<mat-option *ngFor="let type of formPropertyTypes" [value]="type">
{{ formPropertyTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
@ -34,7 +34,7 @@
mat-icon-button
#editButton
(click)="editProperty($event, editButton)"
matTooltip="{{ 'scada.property.property-settings' | translate }}"
matTooltip="{{ 'dynamic-form.property.property-settings' | translate }}"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>
@ -42,7 +42,7 @@
type="button"
mat-icon-button
(click)="propertyRemoved.emit()"
matTooltip="{{ 'scada.property.remove-property' | translate }}"
matTooltip="{{ 'dynamic-form.property.remove-property' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>

2
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.scss

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-scada-symbol-metadata-property-row {
.tb-dynamic-form-property-row {
.tb-id-field {
flex: 1 1 40%;
}

98
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.ts

@ -40,73 +40,43 @@ import {
ValidatorFn,
Validators
} from '@angular/forms';
import {
ScadaSymbolProperty,
ScadaSymbolPropertyType,
scadaSymbolPropertyTypes,
scadaSymbolPropertyTypeTranslations
} from '@home/components/widget/lib/scada/scada-symbol.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';
defaultPropertyValue,
FormProperty,
FormPropertyType,
formPropertyTypes,
formPropertyTypeTranslations, propertyValid
} from '@shared/models/dynamic-form.models';
import {
DynamicFormPropertiesComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component';
import {
ScadaSymbolPropertiesComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-properties.component';
DynamicFormPropertyPanelComponent
} 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: ScadaSymbolProperty): boolean => !(!property.id || !property.name || !property.type);
export const defaultPropertyValue = (type: ScadaSymbolPropertyType): any => {
switch (type) {
case ScadaSymbolPropertyType.text:
return '';
case ScadaSymbolPropertyType.number:
return 0;
case ScadaSymbolPropertyType.switch:
return false;
case ScadaSymbolPropertyType.color:
return '#000';
case ScadaSymbolPropertyType.color_settings:
return constantColor('#000');
case ScadaSymbolPropertyType.font:
return {
size: 12,
sizeUnit: 'px',
family: 'Roboto',
weight: 'normal',
style: 'normal',
lineHeight: '1'
} as Font;
case ScadaSymbolPropertyType.units:
return '';
case ScadaSymbolPropertyType.icon:
return 'star';
}
};
@Component({
selector: 'tb-scada-symbol-metadata-property-row',
templateUrl: './scada-symbol-property-row.component.html',
styleUrls: ['./scada-symbol-property-row.component.scss'],
selector: 'tb-dynamic-form-property-row',
templateUrl: './dynamic-form-property-row.component.html',
styleUrls: ['./dynamic-form-property-row.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent),
useExisting: forwardRef(() => DynamicFormPropertyRowComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ScadaSymbolPropertyRowComponent),
useExisting: forwardRef(() => DynamicFormPropertyRowComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, OnInit, Validator {
export class DynamicFormPropertyRowComponent implements ControlValueAccessor, OnInit, Validator {
@ViewChild('idInput')
idInput: ElementRef<HTMLInputElement>;
@ -114,8 +84,8 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
@ViewChild('editButton')
editButton: MatButton;
scadaSymbolPropertyTypes = scadaSymbolPropertyTypes;
scadaSymbolPropertyTypeTranslations = scadaSymbolPropertyTypeTranslations;
formPropertyTypes = formPropertyTypes;
formPropertyTypeTranslations = formPropertyTypeTranslations;
@Input()
disabled: boolean;
@ -131,7 +101,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
propertyRowFormGroup: UntypedFormGroup;
modelValue: ScadaSymbolProperty;
modelValue: FormProperty;
private propagateChange = (_val: any) => {};
@ -140,7 +110,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private propertiesComponent: ScadaSymbolPropertiesComponent,
private propertiesComponent: DynamicFormPropertiesComponent,
private destroyRef: DestroyRef) {
}
@ -157,7 +127,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
);
this.propertyRowFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((newType: ScadaSymbolPropertyType) => {
).subscribe((newType: FormPropertyType) => {
this.onTypeChanged(newType);
});
}
@ -166,7 +136,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
@ -178,7 +148,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
}
}
writeValue(value: ScadaSymbolProperty): void {
writeValue(value: FormProperty): void {
this.modelValue = value;
this.propertyRowFormGroup.patchValue(
{
@ -204,14 +174,14 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
booleanPropertyIds: this.booleanPropertyIds,
property: deepClone(this.modelValue)
};
const scadaSymbolPropertyPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ScadaSymbolPropertyPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
const dynamicFormPropertyPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, DynamicFormPropertyPanelComponent, ['leftOnly', 'leftTopOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
scadaSymbolPropertyPanelPopover.tbComponentRef.instance.popover = scadaSymbolPropertyPanelPopover;
scadaSymbolPropertyPanelPopover.tbComponentRef.instance.propertySettingsApplied.subscribe((property) => {
scadaSymbolPropertyPanelPopover.hide();
dynamicFormPropertyPanelPopover.tbComponentRef.instance.popover = dynamicFormPropertyPanelPopover;
dynamicFormPropertyPanelPopover.tbComponentRef.instance.propertySettingsApplied.subscribe((property) => {
dynamicFormPropertyPanelPopover.hide();
this.propertyRowFormGroup.patchValue(
{
id: property.id,
@ -222,7 +192,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
this.modelValue = property;
this.propagateChange(this.modelValue);
});
scadaSymbolPropertyPanelPopover.tbDestroy.subscribe(() => {
dynamicFormPropertyPanelPopover.tbDestroy.subscribe(() => {
if (!propertyValid(this.modelValue)) {
editCanceled();
}
@ -240,7 +210,7 @@ export class ScadaSymbolPropertyRowComponent 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});
@ -251,7 +221,7 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
propertyIdNotUnique: true
};
}
const property: ScadaSymbolProperty = {...this.modelValue, ...this.propertyRowFormGroup.value};
const property: FormProperty = {...this.modelValue, ...this.propertyRowFormGroup.value};
if (!propertyValid(property)) {
return {
property: true
@ -276,13 +246,13 @@ export class ScadaSymbolPropertyRowComponent implements ControlValueAccessor, On
};
}
private onTypeChanged(newType: ScadaSymbolPropertyType) {
private onTypeChanged(newType: FormPropertyType) {
this.modelValue = {...this.modelValue, ...{type: newType}};
this.modelValue.default = defaultPropertyValue(newType);
}
private updateModel() {
const value: ScadaSymbolProperty = this.propertyRowFormGroup.value;
const value: FormProperty = this.propertyRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
this.propagateChange(this.modelValue);
}

37
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.html

@ -0,0 +1,37 @@
<!--
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.
-->
<div [formGroup]="selectItemRowFormGroup" class="tb-form-table-row tb-dynamic-form-select-item-row">
<tb-value-input class="tb-value-field"
shortBooleanField
[allowedValueTypes]="[ValueType.STRING, ValueType.INTEGER, ValueType.DOUBLE, ValueType.BOOLEAN]"
formControlName="value">
</tb-value-input>
<mat-form-field class="tb-inline-field tb-label-field" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-form-table-row-cell-buttons">
<button *ngIf="!disabled"
type="button"
mat-icon-button
(click)="selectItemRemoved.emit()"
matTooltip="{{ 'dynamic-form.property.remove-option' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>

11
ui-ngx/src/app/shared/components/json-form/json-form.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.scss

@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-json-form {
padding: 12px;
padding-bottom: 14px !important;
overflow: auto;
.tb-dynamic-form-select-item-row {
.tb-value-field {
flex: 1 1 70%;
}
.tb-label-field {
flex: 1 1 30%;
}
}

171
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component.ts

@ -0,0 +1,171 @@
///
/// 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, DestroyRef,
EventEmitter,
forwardRef,
Input,
OnInit,
Output,
ViewEncapsulation
} from '@angular/core';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
import { isDefinedAndNotNull } from '@core/utils';
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 { ValueType } from '@shared/models/constants';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export const selectItemValid = (item: FormSelectItem): boolean => isDefinedAndNotNull(item.value) && !!item.label;
@Component({
selector: 'tb-dynamic-form-select-item-row',
templateUrl: './dynamic-form-select-item-row.component.html',
styleUrls: ['./dynamic-form-select-item-row.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DynamicFormSelectItemRowComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DynamicFormSelectItemRowComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class DynamicFormSelectItemRowComponent implements ControlValueAccessor, OnInit, Validator {
ValueType = ValueType;
@Input()
disabled: boolean;
@Input()
index: number;
@Output()
selectItemRemoved = new EventEmitter();
selectItemRowFormGroup: UntypedFormGroup;
modelValue: FormSelectItem;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef,
private cd: ChangeDetectorRef,
private selectItemsComponent: DynamicFormSelectItemsComponent) {
}
ngOnInit() {
this.selectItemRowFormGroup = this.fb.group({
value: [null, [this.selectItemValueValidator()]],
label: [null, [Validators.required]]
});
this.selectItemRowFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => this.updateModel()
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.selectItemRowFormGroup.disable({emitEvent: false});
} else {
this.selectItemRowFormGroup.enable({emitEvent: false});
}
}
writeValue(value: FormSelectItem): void {
this.modelValue = value;
this.selectItemRowFormGroup.patchValue(
{
value: value?.value,
label: value?.label
}, {emitEvent: false}
);
this.cd.markForCheck();
}
public validate(_c: UntypedFormControl) {
const valueControl = this.selectItemRowFormGroup.get('value');
if (valueControl.hasError('itemValueNotUnique')) {
valueControl.updateValueAndValidity({onlySelf: false, emitEvent: false});
}
if (valueControl.hasError('itemValueNotUnique')) {
this.selectItemRowFormGroup.get('value').markAsTouched();
return {
itemValueNotUnique: true
};
}
const item: FormSelectItem = {...this.modelValue, ...this.selectItemRowFormGroup.value};
if (!selectItemValid(item)) {
return {
selectItem: true
};
}
return null;
}
private selectItemValueValidator(): ValidatorFn {
return control => {
if (!control.value) {
return {
required: true
};
}
if (!this.selectItemsComponent.selectItemValueUnique(control.value, this.index)) {
return {
itemValueNotUnique: true
};
}
return null;
};
}
private updateModel() {
const value: FormSelectItem = this.selectItemRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
this.propagateChange(this.modelValue);
}
}

33
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.html

@ -15,27 +15,26 @@
limitations under the License.
-->
<div class="tb-form-panel tb-scada-symbol-properties">
<div class="tb-form-panel tb-dynamic-form-select-items no-border no-padding">
<div class="tb-form-table">
<div class="tb-form-table-header" [class.no-padding-right]="!disabled">
<div class="tb-form-table-header-cell tb-id-header" translate>scada.property.id</div>
<div class="tb-form-table-header-cell tb-name-header" translate>scada.property.name</div>
<div class="tb-form-table-header-cell tb-type-header" translate>scada.property.type</div>
<div class="tb-form-table-header-cell tb-value-header" translate>dynamic-form.property.value</div>
<div class="tb-form-table-header-cell tb-label-header" translate>dynamic-form.property.label</div>
<div class="tb-form-table-header-cell tb-actions-header" [class.disabled]="disabled"></div>
</div>
<div *ngIf="propertiesFormArray().controls.length; else noProperties" class="tb-form-table-body tb-drop-list"
<div *ngIf="selectItemsFormArray().controls.length; else noSelectItems" class="tb-form-table-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="propertyDrop($event)">
(cdkDropListDropped)="selectItemDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
[class.tb-draggable]="!disabled"
[class.tb-draggable-form-table-row]="!disabled"
*ngFor="let propertyControl of propertiesFormArray().controls; trackBy: trackByProperty; let $index = index;">
<tb-scada-symbol-metadata-property-row class="flex-1"
[index]="$index"
[booleanPropertyIds]="booleanPropertyIds"
[formControl]="propertyControl"
(propertyRemoved)="removeProperty($index)">
</tb-scada-symbol-metadata-property-row>
*ngFor="let selectItemControl of selectItemsFormArray().controls; trackBy: trackBySelectItem; let $index = index;">
<tb-dynamic-form-select-item-row class="flex-1"
[index]="$index"
[formControl]="selectItemControl"
(selectItemRemoved)="removeSelectItem($index)">
</tb-dynamic-form-select-item-row>
<div class="tb-form-table-row-cell-buttons">
<button mat-icon-button
*ngIf="!disabled"
@ -52,12 +51,12 @@
<tb-error *ngIf="errorText" noMargin [error]="errorText" style="padding-left: 12px;"></tb-error>
</div>
<div *ngIf="!disabled">
<button type="button" mat-stroked-button color="primary" (click)="addProperty()">
{{ 'scada.property.add-property' | translate }}
<button type="button" mat-stroked-button color="primary" (click)="addSelectItem()">
{{ 'dynamic-form.property.add-option' | translate }}
</button>
</div>
</div>
<ng-template #noProperties>
<span class="tb-prompt flex items-center justify-center">{{ 'scada.property.no-properties' | translate }}</span>
<ng-template #noSelectItems>
<span class="tb-prompt flex items-center justify-center">{{ 'dynamic-form.property.no-options' | translate }}</span>
</ng-template>

38
ui-ngx/src/app/shared/components/json-form/react/json-form-help.tsx → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.scss

@ -1,4 +1,4 @@
/*
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -13,15 +13,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
class ThingsboardHelp extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
render() {
return (
<div className={this.props.form.htmlClass} dangerouslySetInnerHTML={{__html: this.props.form.description}} ></div>
);
.tb-dynamic-form-select-items {
flex: 1;
.tb-form-table-header-cell {
&.tb-value-header {
flex: 1 1 70%;
}
&.tb-label-header {
flex: 1 1 30%;
}
&.tb-actions-header {
width: 80px;
min-width: 80px;
&.disabled {
width: 0;
min-width: 0;
}
}
}
.tb-form-table {
overflow: hidden;
}
.tb-form-table-body {
overflow: auto;
tb-dynamic-form-select-item-row {
overflow: hidden;
}
}
}
export default ThingsboardHelp;

204
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component.ts

@ -0,0 +1,204 @@
///
/// 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,
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 { CdkDragDrop } from '@angular/cdk/drag-drop';
import { TranslateService } from '@ngx-translate/core';
import { FormSelectItem } from '@shared/models/dynamic-form.models';
import {
DynamicFormSelectItemRowComponent,
selectItemValid
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-item-row.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-dynamic-form-select-items',
templateUrl: './dynamic-form-select-items.component.html',
styleUrls: ['./dynamic-form-select-items.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DynamicFormSelectItemsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DynamicFormSelectItemsComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class DynamicFormSelectItemsComponent implements ControlValueAccessor, OnInit, Validator {
@HostBinding('style.display') styleDisplay = 'flex';
@HostBinding('style.overflow') styleOverflow = 'hidden';
@ViewChildren(DynamicFormSelectItemRowComponent)
selectItemRows: QueryList<DynamicFormSelectItemRowComponent>;
@Input()
disabled: boolean;
selectItemsFormGroup: UntypedFormGroup;
errorText = '';
get dragEnabled(): boolean {
return !this.disabled && this.selectItemsFormArray().controls.length > 1;
}
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef,
private translate: TranslateService) {
}
ngOnInit() {
this.selectItemsFormGroup = this.fb.group({
selectItems: this.fb.array([])
});
this.selectItemsFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => {
let items: FormSelectItem[] = this.selectItemsFormGroup.get('selectItems').value;
if (items) {
items = items.filter(i => selectItemValid(i));
}
this.propagateChange(items);
}
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.selectItemsFormGroup.disable({emitEvent: false});
} else {
this.selectItemsFormGroup.enable({emitEvent: false});
}
}
writeValue(value: FormSelectItem[] | undefined): void {
const items= value || [];
this.selectItemsFormGroup.setControl('selectItems', this.prepareSelectItemsFormArray(items), {emitEvent: false});
}
public validate(_c: UntypedFormControl) {
this.errorText = '';
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray;
const notUniqueControls =
itemsArray.controls.filter(control => control.hasError('itemValueNotUnique'));
for (const control of notUniqueControls) {
control.updateValueAndValidity({onlySelf: false, emitEvent: false});
if (control.hasError('itemValueNotUnique')) {
this.errorText = this.translate.instant('dynamic-form.property.not-unique-select-option-value-error');
}
}
let valid = this.selectItemsFormGroup.valid;
if (valid) {
const items: FormSelectItem[] = this.selectItemsFormGroup.get('selectItems').value;
valid = !items.some(item => !selectItemValid(item));
}
return valid ? null : {
selectItems: {
valid: false,
},
};
}
public selectItemValueUnique(value: any, index: number): boolean {
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray;
for (let i = 0; i < itemsArray.controls.length; i++) {
if (i !== index) {
const otherControl = itemsArray.controls[i];
if (value === otherControl.value.value) {
return false;
}
}
}
return true;
}
selectItemDrop(event: CdkDragDrop<string[]>) {
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray;
const item = itemsArray.at(event.previousIndex);
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 {
return selectItemControl;
}
removeSelectItem(index: number, emitEvent = true) {
(this.selectItemsFormGroup.get('selectItems') as UntypedFormArray).removeAt(index, {emitEvent});
}
addSelectItem() {
const item: FormSelectItem = {
value: '',
label: ''
};
const itemsArray = this.selectItemsFormGroup.get('selectItems') as UntypedFormArray;
const itemControl = this.fb.control(item, []);
itemsArray.push(itemControl);
}
private prepareSelectItemsFormArray(items: FormSelectItem[] | undefined): UntypedFormArray {
const selectItemsControls: Array<AbstractControl> = [];
if (items) {
items.forEach((item) => {
selectItemsControls.push(this.fb.control(item, []));
});
}
return this.fb.array(selectItemsControls);
}
}

266
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html

@ -0,0 +1,266 @@
<!--
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.
-->
<div [class.!hidden]="!propertyGroups?.length && !propertiesContent.children?.length" class="tb-form-panel"
[class.stroked]="stroked" [class.no-padding]="noPadding" [class.no-border]="noBorder"
[formGroup]="propertiesFormGroup">
<div *ngIf="title" class="tb-form-panel-title">{{ title }}</div>
<ng-container #propertiesContent><ng-content select=".tb-properties-content"></ng-content></ng-container>
<ng-container *ngFor="let propertyGroup of propertyGroups">
<ng-container *ngIf="propertyGroup.visible">
<div *ngIf="propertyGroup.title" class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<div class="tb-form-panel-title">{{ propertyGroup.title | customTranslate }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<ng-container *ngTemplateOutlet="propertyContainersTpl; context:{propertyContainers: propertyGroup.containers}"></ng-container>
</ng-template>
</mat-expansion-panel>
</div>
<ng-container *ngIf="!propertyGroup.title">
<ng-container *ngTemplateOutlet="propertyContainersTpl; context:{propertyContainers: propertyGroup.containers}"></ng-container>
</ng-container>
</ng-container>
</ng-container>
</div>
<ng-template #propertyContainersTpl let-propertyContainers="propertyContainers">
<ng-container *ngFor="let propertyContainer of propertyContainers" [formGroup]="propertiesFormGroup">
<ng-container *ngIf="propertyContainer.visible">
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.field">
<ng-container *ngTemplateOutlet="propertyFieldTpl;context:{property: propertyContainer.property}">
</ng-container>
</ng-container>
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.row">
<ng-container *ngTemplateOutlet="propertyRowTpl;context:{propertyRow: propertyContainer}">
</ng-container>
</ng-container>
<tb-dynamic-form *ngIf="propertyContainer.type === FormPropertyContainerType.fieldset"
formControlName="{{ propertyContainer.property.id }}"
title="{{ propertyContainer.label | customTranslate }}"
stroked
[properties]="propertyContainer.properties">
</tb-dynamic-form>
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.array">
<div *ngIf="propertyContainer.arrayItemProperty.type === FormPropertyType.image" class="tb-form-row flex-1">
<tb-multiple-gallery-image-input
class="flex-1"
label="{{ propertyContainer.property.name | customTranslate }}"
formControlName="{{ propertyContainer.property.id }}"
[required]="propertyContainer.property.required">
</tb-multiple-gallery-image-input>
</div>
<tb-dynamic-form-array *ngIf="propertyContainer.arrayItemProperty.type !== FormPropertyType.image"
formControlName="{{ propertyContainer.property.id }}"
[itemProperty]="propertyContainer.arrayItemProperty"
title="{{ propertyContainer.label | customTranslate }}">
</tb-dynamic-form-array>
</ng-container>
<ng-container *ngIf="propertyContainer.type === FormPropertyContainerType.htmlSection">
<div [innerHTML]="propertyContainer.safeHtml" [class]="propertyContainer.htmlClass"></div>
</ng-container>
</ng-container>
</ng-container>
</ng-template>
<ng-template #propertyFieldTpl let-property="property">
<div [formGroup]="propertiesFormGroup" class="tb-form-row no-border no-padding tb-standard-fields">
<ng-container *ngTemplateOutlet="inputFieldTpl;context:{property: property, singleField: true}">
</ng-container>
</div>
</ng-template>
<ng-template #propertyRowTpl let-propertyRow="propertyRow">
<div [formGroup]="propertiesFormGroup" class="tb-form-row space-between overflow-auto" [class]="propertyRow.rowClass">
<mat-slide-toggle *ngIf="propertyRow.switch && propertyRow.switch.visible"
class="mat-slide fixed-title-width margin" formControlName="{{ propertyRow.switch.id }}">
{{ propertyRow.label | customTranslate }}
</mat-slide-toggle>
<div *ngIf="!propertyRow.switch" class="fixed-title-width fixed-title-height">{{ propertyRow.label | customTranslate }}</div>
<div *ngIf="propertyRow.properties.length" class="tb-flex" [class]="propertyRow.propertiesRowClass">
<ng-container *ngFor="let property of propertyRow.properties">
<ng-container *ngIf="property.visible">
<div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel | customTranslate }}</div>
<ng-container *ngIf="isInputFieldPropertyType(property.type)">
<ng-container *ngTemplateOutlet="inputFieldTpl;context:{property: property, singleField: false}">
</ng-container>
</ng-container>
<tb-color-input *ngIf="property.type === FormPropertyType.color"
[required]="property.required"
[class]="property.fieldClass"
asBoxInput
colorClearButton
formControlName="{{ property.id }}">
</tb-color-input>
<tb-color-settings *ngIf="property.type === FormPropertyType.color_settings"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
settingsKey="{{ property.name | customTranslate }}">
</tb-color-settings>
<tb-font-settings *ngIf="property.type === FormPropertyType.font"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
clearButton
disabledLineHeight>
</tb-font-settings>
<tb-unit-input *ngIf="property.type === FormPropertyType.units"
[class]="property.fieldClass"
[required]="property.required"
formControlName="{{ property.id }}"></tb-unit-input>
<tb-material-icon-select *ngIf="property.type === FormPropertyType.icon"
[required]="property.required"
asBoxInput
formControlName="{{ property.id }}">
</tb-material-icon-select>
<mat-divider *ngIf="property.divider" vertical></mat-divider>
</ng-container>
</ng-container>
</div>
</div>
</ng-template>
<ng-template #inputFieldTpl let-property="property" let-singleField="singleField">
<ng-container [formGroup]="propertiesFormGroup">
<mat-form-field *ngIf="property.type === FormPropertyType.textarea"
[class]="singleField ? 'flex' : property.fieldClass"
[floatLabel]="singleField ? 'always' : 'auto'"
appearance="outline" subscriptSizing="dynamic">
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label>
<textarea matInput
[rows]="property.rows || 2"
formControlName="{{ property.id }}"
[required]="property.required"
placeholder="{{ 'widget-config.set' | translate }}"></textarea>
</mat-form-field>
<mat-form-field *ngIf="[FormPropertyType.text, FormPropertyType.password].includes(property.type)"
[class]="singleField ? 'flex' : property.fieldClass"
[floatLabel]="singleField ? 'always' : 'auto'"
appearance="outline" subscriptSizing="dynamic">
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label>
<input matInput
[type]="property.type"
formControlName="{{ property.id }}"
[required]="property.required"
placeholder="{{ 'widget-config.set' | translate }}">
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field>
<mat-form-field *ngIf="property.type === FormPropertyType.number"
[class]="singleField ? 'flex' : property.fieldClass"
class="number"
[floatLabel]="singleField ? 'always' : 'auto'"
appearance="outline" subscriptSizing="dynamic">
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label>
<input matInput
type="number"
formControlName="{{ property.id }}"
[required]="property.required"
[min]="property.min"
[max]="property.max"
[step]="property.step"
placeholder="{{ 'widget-config.set' | translate }}">
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field>
<mat-form-field *ngIf="property.type === FormPropertyType.select"
[class]="singleField ? 'flex' : (property.fieldClass || 'medium-width')"
[floatLabel]="singleField ? 'always' : 'auto'"
appearance="outline" subscriptSizing="dynamic">
<mat-label *ngIf="singleField">{{ property.name | customTranslate }}</mat-label>
<mat-select formControlName="{{ property.id }}"
[required]="property.required"
[multiple]="property.multiple">
<mat-option *ngIf="!property.multiple && property.allowEmptyOption" [value]="null"></mat-option>
<mat-option *ngFor="let item of property.items" [value]="item.value">
{{ item.label | customTranslate }}
</mat-option>
</mat-select>
</mat-form-field>
<tb-datetime *ngIf="property.type === FormPropertyType.datetime"
[fieldClass]="singleField ? 'flex' : property.fieldClass"
[class.flex-1]="singleField || property.fieldClass?.includes('flex')"
[floatLabel]="singleField ? 'always' : 'auto'"
[showLabel]="singleField"
[dateText]="singleField ? (property.name | customTranslate) : ''"
[allowClear]="property.allowClear"
[required]="property.required"
appearance="outline"
subscriptSizing="dynamic"
[type]="property.dateTimeType"
formControlName="{{ property.id }}">
</tb-datetime>
<div *ngIf="property.type === FormPropertyType.javascript" class="tb-form-row flex-1">
<tb-js-func class="flex-1"
label="{{ property.name | customTranslate }}"
disableUndefinedCheck
[required]="property.required"
[helpId]="property.helpId"
formControlName="{{ property.id }}">
</tb-js-func>
</div>
<div *ngIf="property.type === FormPropertyType.json" class="tb-form-row flex-1">
<tb-json-content class="flex-1"
label="{{ property.name | customTranslate }}"
[required]="property.required"
[contentType]="ContentType.JSON"
formControlName="{{ property.id }}">
</tb-json-content>
</div>
<div *ngIf="property.type === FormPropertyType.html" class="tb-form-row flex-1">
<tb-html class="flex-1"
label="{{ property.name | customTranslate }}"
[required]="property.required"
formControlName="{{ property.id }}">
</tb-html>
</div>
<div *ngIf="property.type === FormPropertyType.css" class="tb-form-row flex-1">
<tb-css class="flex-1"
label="{{ property.name | customTranslate }}"
[required]="property.required"
formControlName="{{ property.id }}">
</tb-css>
</div>
<div *ngIf="property.type === FormPropertyType.markdown" class="tb-form-row flex-1">
<tb-markdown-editor class="flex-1"
label="{{ property.name | customTranslate }}"
[required]="property.required"
[helpId]="property.helpId"
formControlName="{{ property.id }}">
</tb-markdown-editor>
</div>
<div *ngIf="property.type === FormPropertyType.image" class="tb-form-row flex-1">
<tb-gallery-image-input
class="flex-1"
label="{{ property.name | customTranslate }}"
formControlName="{{ property.id }}"
[required]="property.required">
</tb-gallery-image-input>
</div>
<div *ngIf="property.type === FormPropertyType.radios"
class="tb-form-row tb-radios-property column-xs flex-1" [class.direction-column]="property.direction === 'column'">
<div tbTruncateWithTooltip class="fixed-title-width fixed-title-height">{{ property.name | customTranslate }}</div>
<mat-radio-group class="flex flex-1 flex-wrap" [class.flex-col]="property.direction === 'column'"
[required]="property.required"
formControlName="{{ property.id }}">
<mat-radio-button *ngFor="let item of property.items" [value]="item.value">
<div tbTruncateWithTooltip>{{ item.label | customTranslate }}</div>
</mat-radio-button>
</mat-radio-group>
</div>
</ng-container>
</ng-template>

40
ui-ngx/src/app/shared/components/json-form/react/json-form-markdown.tsx → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.scss

@ -1,4 +1,4 @@
/*
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -13,21 +13,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
class ThingsboardMarkdown extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
constructor(props: JsonFormFieldProps) {
super(props);
:host ::ng-deep {
.tb-properties-content {
gap: 16px;
display: flex;
flex-direction: column;
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='markdown' {...this.state}></ThingsboardAceEditor>
);
.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;
}
}
}
}
}
}
export default ThingsboardMarkdown;

301
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.ts

@ -0,0 +1,301 @@
///
/// 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,
DestroyRef,
forwardRef,
Input,
OnChanges,
OnInit,
SimpleChanges
} from '@angular/core';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
import {
defaultFormProperties,
FormProperty,
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',
templateUrl: './dynamic-form.component.html',
styleUrls: ['./dynamic-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DynamicFormComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DynamicFormComponent),
multi: true
}
]
})
export class DynamicFormComponent implements OnInit, OnChanges, ControlValueAccessor, Validator {
isInputFieldPropertyType = isInputFieldPropertyType;
FormPropertyContainerType = FormPropertyContainerType;
FormPropertyType = FormPropertyType;
ContentType = ContentType;
@Input()
disabled: boolean;
@Input()
properties: FormProperty[];
@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;
private validatorTriggers: string[];
public propertiesFormGroup: UntypedFormGroup;
propertyGroups: FormPropertyGroup[];
constructor(protected store: Store<AppState>,
private customTranslate: CustomTranslatePipe,
private sanitizer: DomSanitizer,
private destroyRef: DestroyRef,
private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
this.propertiesFormGroup = this.fb.group({
});
this.propertiesFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateModel();
});
this.loadMetadata();
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['properties'].includes(propName)) {
this.loadMetadata();
}
}
}
}
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});
this.updateControlsState();
}
}
writeValue(value: {[id: string]: any}): void {
this.modelValue = value || {};
this.setupValue();
}
validate(_c: UntypedFormControl) {
const valid = this.propertiesFormGroup.valid;
return valid ? null : {
properties: {
valid: false,
},
};
}
private loadMetadata() {
this.validatorTriggers = [];
this.propertyGroups = [];
for (const control of Object.keys(this.propertiesFormGroup.controls)) {
this.propertiesFormGroup.removeControl(control, {emitEvent: false});
}
if (this.properties) {
for (let property of this.properties) {
property.disabled = false;
property.visible = true;
if (property.condition) {
try {
property.conditionFunction = new Function('property', 'model', property.condition) as PropertyConditionFunction;
} catch (_e) {
}
}
}
this.propertyGroups = toPropertyGroups(this.properties, this.isArrayItem, this.customTranslate, this.sanitizer);
for (const property of this.properties) {
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));
}
if (isDefinedAndNotNull(property.max)) {
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});
}
}
}
this.setupValue();
this.cd.markForCheck();
}
private calculateControlsState(updateControls = false) {
for (const trigger of this.validatorTriggers) {
const value: boolean = this.propertiesFormGroup.get(trigger).value;
this.properties.filter(p => p.disableOnProperty === trigger).forEach(
(p) => {
p.disabled = !value;
}
);
}
if (this.properties) {
for (let property of this.properties) {
if (property.conditionFunction) {
property.visible = property.conditionFunction(property, this.modelValue);
}
}
this.propertyGroups.forEach(g => {
g.containers.forEach(container => {
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);
}
});
g.visible = g.containers.some(c => c.visible);
});
}
if (updateControls) {
this.updateControlsState();
}
}
private updateControlsState() {
if (this.properties) {
for (let property of this.properties) {
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});
}
}
}
}
}
private setupValue() {
if (this.properties) {
const defaults = defaultFormProperties(this.properties);
this.modelValue = mergeDeep<{[id: string]: any}>(defaults, this.modelValue);
this.propertiesFormGroup.patchValue(
this.modelValue, {emitEvent: false}
);
this.calculateControlsState();
this.setDisabledState(this.disabled);
}
}
private updateModel() {
this.modelValue = this.propertiesFormGroup.getRawValue();
this.calculateControlsState(true);
this.propagateChange(this.modelValue);
}
}

57
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html

@ -36,58 +36,11 @@
</ng-container>
</ng-container>
</div>
<div [class.!hidden]="!propertyRows?.length && !appearanceProperties.children.length" class="tb-form-panel" formGroupName="properties">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div #appearanceProperties><ng-content select=".tb-scada-symbol-appearance-properties"></ng-content></div>
<div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between flex-wrap overflow-auto" [class]="propertyRow.rowClass">
<mat-slide-toggle *ngIf="propertyRow.switch" class="mat-slide fixed-title-width margin" formControlName="{{ propertyRow.switch.id }}">
{{ propertyRow.label | customTranslate }}
</mat-slide-toggle>
<div *ngIf="!propertyRow.switch" class="fixed-title-width fixed-title-height">{{ propertyRow.label | customTranslate }}</div>
<div *ngIf="propertyRow.properties.length" class="tb-flex row flex-end align-center">
<ng-container *ngFor="let property of propertyRow.properties">
<div *ngIf="property.subLabel" class="tb-small-label">{{ property.subLabel | customTranslate }}</div>
<mat-form-field *ngIf="property.type === ScadaSymbolPropertyType.text" [class]="property.fieldClass" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [required]="property.required" placeholder="{{ 'widget-config.set' | translate }}">
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field>
<tb-color-input *ngIf="property.type === ScadaSymbolPropertyType.color"
[required]="property.required"
[class]="property.fieldClass"
asBoxInput
colorClearButton
formControlName="{{ property.id }}">
</tb-color-input>
<tb-color-settings *ngIf="property.type === ScadaSymbolPropertyType.color_settings"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
settingsKey="{{ property.name | customTranslate }}">
</tb-color-settings>
<mat-form-field *ngIf="property.type === ScadaSymbolPropertyType.number" [class]="property.fieldClass" appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="{{ property.id }}" [required]="property.required"
[min]="property.min" [max]="property.max" [step]="property.step"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
<div class="lt-md:!hidden" matSuffix *ngIf="property.fieldSuffix">{{ property.fieldSuffix | customTranslate }}</div>
</mat-form-field>
<tb-font-settings *ngIf="property.type === ScadaSymbolPropertyType.font"
[class]="property.fieldClass"
formControlName="{{ property.id }}"
clearButton
disabledLineHeight>
</tb-font-settings>
<tb-unit-input *ngIf="property.type === ScadaSymbolPropertyType.units"
[class]="property.fieldClass"
[required]="property.required"
formControlName="{{ property.id }}"></tb-unit-input>
<tb-material-icon-select *ngIf="property.type === ScadaSymbolPropertyType.icon"
asBoxInput
formControlName="{{ property.id }}">
</tb-material-icon-select>
<mat-divider *ngIf="property.divider" vertical></mat-divider>
</ng-container>
</div>
</div>
</div>
<tb-dynamic-form formControlName="properties"
title="{{ 'widget-config.appearance' | translate }}"
[properties]="metadata?.properties">
<ng-container class="tb-properties-content"><ng-content select=".tb-scada-symbol-appearance-properties"></ng-content></ng-container>
</tb-dynamic-form>
</ng-container>
<ng-template #behaviorsTpl let-behaviors="behaviors">

95
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.ts

@ -21,7 +21,6 @@ import {
forwardRef,
Input,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges
} from '@angular/core';
@ -32,9 +31,7 @@ import {
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator,
ValidatorFn,
Validators
Validator
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -43,19 +40,16 @@ import {
parseScadaSymbolMetadataFromContent,
ScadaSymbolBehaviorType,
ScadaSymbolMetadata,
ScadaSymbolObjectSettings,
ScadaSymbolPropertyType
ScadaSymbolObjectSettings
} from '@home/components/widget/lib/scada/scada-symbol.models';
import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { isDefinedAndNotNull, mergeDeep } from '@core/utils';
import { mergeDeep } from '@core/utils';
import {
ScadaSymbolBehaviorGroup,
ScadaSymbolPropertyRow,
toBehaviorGroups,
toPropertyRows
toBehaviorGroups
} from '@home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models';
import { merge, Observable, of, Subscription } from 'rxjs';
import { Observable, of } from 'rxjs';
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models';
import { ImageService } from '@core/http/image.service';
import { map } from 'rxjs/operators';
@ -78,12 +72,10 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
}
]
})
export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator, OnDestroy {
export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, ControlValueAccessor, Validator {
ScadaSymbolBehaviorType = ScadaSymbolBehaviorType;
ScadaSymbolPropertyType = ScadaSymbolPropertyType;
@Input()
disabled: boolean;
@ -112,14 +104,10 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
private propagateChange = null;
private validatorTriggers: string[];
private validatorSubscription: Subscription;
public scadaSymbolObjectSettingsFormGroup: UntypedFormGroup;
metadata: ScadaSymbolMetadata;
behaviorGroups: ScadaSymbolBehaviorGroup[];
propertyRows: ScadaSymbolPropertyRow[];
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder,
@ -131,7 +119,7 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
ngOnInit(): void {
this.scadaSymbolObjectSettingsFormGroup = this.fb.group({
behavior: this.fb.group({}),
properties: this.fb.group({})
properties: [{}, []]
});
this.scadaSymbolObjectSettingsFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
@ -152,13 +140,6 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
}
}
ngOnDestroy() {
if (this.validatorSubscription) {
this.validatorSubscription.unsubscribe();
this.validatorSubscription = null;
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
@ -172,7 +153,6 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
this.scadaSymbolObjectSettingsFormGroup.disable({emitEvent: false});
} else {
this.scadaSymbolObjectSettingsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
@ -191,12 +171,6 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
}
private loadMetadata() {
if (this.validatorSubscription) {
this.validatorSubscription.unsubscribe();
this.validatorSubscription = null;
}
this.validatorTriggers = [];
let metadata$: Observable<ScadaSymbolMetadata>;
if (this.scadaSymbolMetadata) {
metadata$ = of(this.scadaSymbolMetadata);
@ -217,74 +191,19 @@ export class ScadaSymbolObjectSettingsComponent implements OnInit, OnChanges, Co
(metadata) => {
this.metadata = metadata;
this.behaviorGroups = toBehaviorGroups(this.metadata.behavior);
this.propertyRows = toPropertyRows(this.metadata.properties);
const behaviorFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('behavior') as UntypedFormGroup;
for (const control of Object.keys(behaviorFormGroup.controls)) {
behaviorFormGroup.removeControl(control, {emitEvent: false});
}
const propertiesFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('properties') as UntypedFormGroup;
for (const control of Object.keys(propertiesFormGroup.controls)) {
propertiesFormGroup.removeControl(control, {emitEvent: false});
}
for (const behavior of this.metadata.behavior) {
behaviorFormGroup.addControl(behavior.id, this.fb.control(null, []), {emitEvent: false});
}
for (const property of this.metadata.properties) {
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 === ScadaSymbolPropertyType.number) {
if (isDefinedAndNotNull(property.min)) {
validators.push(Validators.min(property.min));
}
if (isDefinedAndNotNull(property.max)) {
validators.push(Validators.max(property.max));
}
}
propertiesFormGroup.addControl(property.id, this.fb.control(null, validators), {emitEvent: false});
}
if (this.validatorTriggers.length) {
const observables: Observable<any>[] = [];
for (const trigger of this.validatorTriggers) {
if (propertiesFormGroup.get(trigger)) {
observables.push(propertiesFormGroup.get(trigger).valueChanges);
}
}
if (observables.length) {
this.validatorSubscription = merge(...observables).subscribe(() => {
this.updateValidators();
});
}
}
this.setupValue();
this.cd.markForCheck();
}
);
}
private updateValidators() {
const propertiesFormGroup = this.scadaSymbolObjectSettingsFormGroup.get('properties') as UntypedFormGroup;
for (const trigger of this.validatorTriggers) {
const value: boolean = propertiesFormGroup.get(trigger).value;
this.metadata.properties.filter(p => p.disableOnProperty === trigger).forEach(
(p) => {
const control = propertiesFormGroup.get(p.id);
if (value) {
control.enable({emitEvent: false});
} else {
control.disable({emitEvent: false});
}
}
);
}
}
private setupValue() {
if (this.metadata) {
const defaults = defaultScadaSymbolObjectSettings(this.metadata);

32
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.models.ts

@ -15,9 +15,7 @@
///
import {
ScadaSymbolBehavior,
ScadaSymbolProperty,
ScadaSymbolPropertyType
ScadaSymbolBehavior
} from '@home/components/widget/lib/scada/scada-symbol.models';
export interface ScadaSymbolBehaviorGroup {
@ -25,13 +23,6 @@ export interface ScadaSymbolBehaviorGroup {
behaviors: ScadaSymbolBehavior[];
}
export interface ScadaSymbolPropertyRow {
label: string;
properties: ScadaSymbolProperty[];
switch?: ScadaSymbolProperty;
rowClass?: string;
}
export const toBehaviorGroups = (behaviors: ScadaSymbolBehavior[]): ScadaSymbolBehaviorGroup[] => {
const result: ScadaSymbolBehaviorGroup[] = [];
for (const behavior of behaviors) {
@ -54,24 +45,3 @@ export const toBehaviorGroups = (behaviors: ScadaSymbolBehavior[]): ScadaSymbolB
}
return result;
};
export const toPropertyRows = (properties: ScadaSymbolProperty[]): ScadaSymbolPropertyRow[] => {
const result: ScadaSymbolPropertyRow[] = [];
for (const property of properties) {
let propertyRow = result.find(r => r.label === property.name);
if (!propertyRow) {
propertyRow = {
label: property.name,
properties: [],
rowClass: property.rowClass
};
result.push(propertyRow);
}
if (property.type === ScadaSymbolPropertyType.switch) {
propertyRow.switch = property;
} else {
propertyRow.properties.push(property);
}
}
return result;
};

37
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -164,6 +164,25 @@ import {
import {
WidgetButtonToggleCustomStylePanelComponent
} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component';
import {
DynamicFormPropertiesComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component';
import {
DynamicFormPropertyRowComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component';
import {
DynamicFormPropertyPanelComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component';
import { DynamicFormComponent } from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component';
import {
DynamicFormSelectItemsComponent
} from '@home/components/widget/lib/settings/common/dynamic-form/dynamic-form-select-items.component';
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: [
@ -226,7 +245,14 @@ import {
DataKeyInputComponent,
EntityAliasInputComponent,
AdvancedRangeComponent,
GradientComponent
GradientComponent,
DynamicFormPropertiesComponent,
DynamicFormPropertyRowComponent,
DynamicFormPropertyPanelComponent,
DynamicFormSelectItemsComponent,
DynamicFormSelectItemRowComponent,
DynamicFormComponent,
DynamicFormArrayComponent
],
imports: [
CommonModule,
@ -293,7 +319,14 @@ import {
DataKeyInputComponent,
EntityAliasInputComponent,
AdvancedRangeComponent,
GradientComponent
GradientComponent,
DynamicFormPropertiesComponent,
DynamicFormPropertyRowComponent,
DynamicFormPropertyPanelComponent,
DynamicFormSelectItemsComponent,
DynamicFormSelectItemRowComponent,
DynamicFormComponent,
DynamicFormArrayComponent
],
providers: [
ColorSettingsComponentService,

69
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -36,7 +36,7 @@ import {
ResourcesService
} from '@core/services/resources.service';
import {
IWidgetSettingsComponent,
IWidgetSettingsComponent, migrateWidgetTypeToDynamicForms,
Widget,
widgetActionSources,
WidgetControllerDescriptor,
@ -63,6 +63,7 @@ import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/b
import { IBasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { compileTbFunction, TbFunction } from '@shared/models/js-function.models';
import { HttpClient } from '@angular/common/http';
import { jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models';
@Injectable()
export class WidgetComponentService {
@ -112,9 +113,9 @@ export class WidgetComponentService {
templateHtml: this.utils.editWidgetInfo.templateHtml,
templateCss: this.utils.editWidgetInfo.templateCss,
controllerScript: this.utils.editWidgetInfo.controllerScript,
settingsSchema: this.utils.editWidgetInfo.settingsSchema,
dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema,
latestDataKeySettingsSchema: this.utils.editWidgetInfo.latestDataKeySettingsSchema,
settingsForm: this.utils.editWidgetInfo.settingsForm,
dataKeySettingsForm: this.utils.editWidgetInfo.dataKeySettingsForm,
latestDataKeySettingsForm: this.utils.editWidgetInfo.latestDataKeySettingsForm,
settingsDirective: this.utils.editWidgetInfo.settingsDirective,
dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: this.utils.editWidgetInfo.latestDataKeySettingsDirective,
@ -275,6 +276,7 @@ export class WidgetComponentService {
this.widgetsInfoFetchQueue.set(fullFqn, fetchQueue);
this.widgetService.getWidgetType(fullFqn, {ignoreErrors: true}).subscribe(
(widgetType) => {
widgetType = migrateWidgetTypeToDynamicForms(widgetType);
this.loadWidget(widgetType, widgetInfoSubject);
},
() => {
@ -298,14 +300,14 @@ export class WidgetComponentService {
this.loadWidgetResources(widgetInfo, widgetNamespace, [SharedModule, WidgetComponentsModule, this.homeComponentsModule]).subscribe(
{
next: () => {
if (widgetControllerDescriptor.settingsSchema) {
widgetInfo.typeSettingsSchema = widgetControllerDescriptor.settingsSchema;
if (widgetControllerDescriptor.settingsForm) {
widgetInfo.typeSettingsForm = widgetControllerDescriptor.settingsForm;
}
if (widgetControllerDescriptor.dataKeySettingsSchema) {
widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema;
if (widgetControllerDescriptor.dataKeySettingsForm) {
widgetInfo.typeDataKeySettingsForm = widgetControllerDescriptor.dataKeySettingsForm;
}
if (widgetControllerDescriptor.latestDataKeySettingsSchema) {
widgetInfo.typeLatestDataKeySettingsSchema = widgetControllerDescriptor.latestDataKeySettingsSchema;
if (widgetControllerDescriptor.latestDataKeySettingsForm) {
widgetInfo.typeLatestDataKeySettingsForm = widgetControllerDescriptor.latestDataKeySettingsForm;
}
widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters;
widgetInfo.actionSources = widgetControllerDescriptor.actionSources;
@ -506,12 +508,23 @@ export class WidgetComponentService {
' }\n\n' +
' self.getSettingsSchema = function() {\n\n' +
' self.getSettingsForm = function() {\n\n' +
return [
{
'id': 'testProp',
'name': 'Test property',
'type': 'text',
'default': 'Default value'
}
];
' }\n\n' +
' self.getDataKeySettingsSchema = function() {\n\n' +
' self.getDataKeySettingsForm = function() {\n\n' +
return [];
' }\n\n' +
' self.getLatestDataKeySettingsForm = function() {\n\n' +
return [];
' }\n\n' +
' self.onDestroy = function() {\n\n' +
@ -539,15 +552,33 @@ export class WidgetComponentService {
const result: WidgetControllerDescriptor = {
widgetTypeFunction: widgetType
};
if (isFunction(widgetTypeInstance.getSettingsSchema)) {
result.settingsSchema = widgetTypeInstance.getSettingsSchema();
if (isFunction(widgetTypeInstance.getSettingsForm)) {
result.settingsForm = widgetTypeInstance.getSettingsForm();
}
if (isFunction(widgetTypeInstance.getDataKeySettingsForm)) {
result.dataKeySettingsForm = widgetTypeInstance.getDataKeySettingsForm();
}
if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
if (isFunction(widgetTypeInstance.getLatestDataKeySettingsForm)) {
result.latestDataKeySettingsForm = widgetTypeInstance.getLatestDataKeySettingsForm();
}
if (isFunction(widgetTypeInstance.getLatestDataKeySettingsSchema)) {
result.latestDataKeySettingsSchema = widgetTypeInstance.getLatestDataKeySettingsSchema();
/** Start migrate from old JSON Schema Form **/
if (isFunction((widgetTypeInstance as any).getSettingsSchema) && !result.settingsForm?.length) {
const settingsSchema = (widgetTypeInstance as any).getSettingsSchema();
result.settingsForm = jsonFormSchemaToFormProperties(settingsSchema);
}
if (isFunction((widgetTypeInstance as any).getDataKeySettingsSchema) && !result.dataKeySettingsForm?.length) {
const dataKeySettingsSchema = (widgetTypeInstance as any).getDataKeySettingsSchema();
result.dataKeySettingsForm = jsonFormSchemaToFormProperties(dataKeySettingsSchema);
}
if (isFunction((widgetTypeInstance as any).getLatestDataKeySettingsSchema) && !result.latestDataKeySettingsForm?.length) {
const latestDataKeySettingsSchema = (widgetTypeInstance as any).getLatestDataKeySettingsSchema();
result.latestDataKeySettingsForm = jsonFormSchemaToFormProperties(latestDataKeySettingsSchema);
}
/** End migrate from old JSON Schema Form **/
if (isFunction(widgetTypeInstance.typeParameters)) {
result.typeParameters = widgetTypeInstance.typeParameters();
} else {

46
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -34,9 +34,7 @@ import {
DataKey,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
GroupInfo,
JsonSchema,
JsonSettingsSchema,
DynamicFormData,
TargetDevice,
targetDeviceValid,
Widget,
@ -75,7 +73,6 @@ import {
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';
import { EntityService } from '@core/http/entity.service';
import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models';
import { Dashboard } from '@shared/models/dashboard.models';
import { entityFields } from '@shared/models/entity.models';
import { Filter, singleEntityFilterFromDeviceId } from '@shared/models/query/query.models';
@ -85,18 +82,10 @@ import { coerceBoolean } from '@shared/decorators/coercion';
import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module';
import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { defaultFormProperties, FormProperty } from '@shared/models/dynamic-form.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import Timeout = NodeJS.Timeout;
const emptySettingsSchema: JsonSchema = {
type: 'object',
properties: {}
};
const emptySettingsGroupInfoes: GroupInfo[] = [];
const defaultSettingsForm = [
'*'
];
@Component({
selector: 'tb-widget-config',
templateUrl: './widget-config.component.html',
@ -571,7 +560,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
}
}
this.updateSchemaForm(config.settings);
this.updateAdvancedForm(config.settings);
if (layout) {
this.layoutSettings.patchValue(
@ -641,21 +630,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
}
}
private updateSchemaForm(settings?: any) {
const widgetSettingsFormData: JsonFormComponentData = {};
if (this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema) {
widgetSettingsFormData.schema = this.modelValue.settingsSchema.schema;
widgetSettingsFormData.form = this.modelValue.settingsSchema.form || deepClone(defaultSettingsForm);
widgetSettingsFormData.groupInfoes = this.modelValue.settingsSchema.groupInfoes;
widgetSettingsFormData.model = settings;
private updateAdvancedForm(settings?: any) {
const dynamicFormData: DynamicFormData = {};
dynamicFormData.model = settings || {};
if (this.modelValue.settingsForm?.length) {
dynamicFormData.settingsForm = this.modelValue.settingsForm;
} else {
widgetSettingsFormData.schema = deepClone(emptySettingsSchema);
widgetSettingsFormData.form = deepClone(defaultSettingsForm);
widgetSettingsFormData.groupInfoes = deepClone(emptySettingsGroupInfoes);
widgetSettingsFormData.model = settings || {};
dynamicFormData.settingsForm = [];
}
widgetSettingsFormData.settingsDirective = this.modelValue.settingsDirective;
this.advancedSettings.patchValue({ settings: widgetSettingsFormData }, {emitEvent: false});
dynamicFormData.settingsDirective = this.modelValue.settingsDirective;
this.advancedSettings.patchValue({ settings: dynamicFormData }, {emitEvent: false});
}
private updateDataSettings() {
@ -731,7 +715,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
}
public get displayAdvancedAppearance(): boolean {
return !!this.modelValue && (!!this.modelValue.settingsSchema && !!this.modelValue.settingsSchema.schema ||
return !!this.modelValue && (!!this.modelValue.settingsForm && !!this.modelValue.settingsForm.length ||
!!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length);
}
@ -770,7 +754,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
}
}
public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema,
public generateDataKey(chip: any, type: DataKeyType, dataKeySettingsForm: FormProperty[],
isLatestDataKey: boolean, dataKeySettingsFunction: DataKeySettingsFunction): DataKey {
if (isObject(chip)) {
(chip as DataKey)._hash = Math.random();
@ -802,8 +786,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
} else if (type === DataKeyType.count) {
result.name = 'count';
}
if (datakeySettingsSchema && isDefined(datakeySettingsSchema.schema)) {
result.settings = this.utils.generateObjectFromJsonSchema(datakeySettingsSchema.schema);
if (dataKeySettingsForm?.length) {
result.settings = defaultFormProperties(dataKeySettingsForm);
} else if (dataKeySettingsFunction) {
const settings = dataKeySettingsFunction(result, isLatestDataKey);
if (settings) {

40
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -21,7 +21,6 @@ import {
DatasourceData,
FormattedData,
fullWidgetTypeFqn,
JsonSettingsSchema,
Widget,
WidgetActionDescriptor,
WidgetActionSource,
@ -106,6 +105,7 @@ import { UserSettingsService } from '@core/http/user-settings.service';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { UtilsService } from '@core/services/utils.service';
import { CompiledTbFunction } from '@shared/models/js-function.models';
import { FormProperty } from '@shared/models/dynamic-form.models';
export interface IWidgetAction {
name: string;
@ -560,9 +560,9 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
fullFqn: string;
deprecated: boolean;
scada: boolean;
typeSettingsSchema?: string | any;
typeDataKeySettingsSchema?: string | any;
typeLatestDataKeySettingsSchema?: string | any;
typeSettingsForm?: FormProperty[];
typeDataKeySettingsForm?: FormProperty[];
typeLatestDataKeySettingsForm?: FormProperty[];
image?: string;
description?: string;
tags?: string[];
@ -577,9 +577,9 @@ export interface WidgetConfigComponentData {
typeParameters: WidgetTypeParameters;
actionSources: {[actionSourceId: string]: WidgetActionSource};
isDataEnabled: boolean;
settingsSchema: JsonSettingsSchema;
dataKeySettingsSchema: JsonSettingsSchema;
latestDataKeySettingsSchema: JsonSettingsSchema;
settingsForm: FormProperty[];
dataKeySettingsForm: FormProperty[];
latestDataKeySettingsForm: FormProperty[];
dataKeySettingsFunction: DataKeySettingsFunction;
settingsDirective: string;
dataKeySettingsDirective: string;
@ -602,8 +602,8 @@ export const MissingWidgetType: WidgetInfo = {
'</div>',
templateCss: '',
controllerScript: 'self.onInit = function() {}',
settingsSchema: '{}\n',
dataKeySettingsSchema: '{}\n',
settingsForm: [],
dataKeySettingsForm: [],
image: null,
description: null,
defaultConfig: '{\n' +
@ -629,8 +629,8 @@ export const ErrorWidgetType: WidgetInfo = {
'</div>',
templateCss: '',
controllerScript: 'self.onInit = function() {}',
settingsSchema: '{}\n',
dataKeySettingsSchema: '{}\n',
settingsForm: [],
dataKeySettingsForm: [],
image: null,
description: null,
defaultConfig: '{\n' +
@ -642,9 +642,9 @@ export const ErrorWidgetType: WidgetInfo = {
};
export interface WidgetTypeInstance {
getSettingsSchema?: () => string;
getDataKeySettingsSchema?: () => string;
getLatestDataKeySettingsSchema?: () => string;
getSettingsForm?: () => FormProperty[];
getDataKeySettingsForm?: () => FormProperty[];
getLatestDataKeySettingsForm?: () => FormProperty[];
typeParameters?: () => WidgetTypeParameters;
useCustomDatasources?: () => boolean;
actionSources?: () => {[actionSourceId: string]: WidgetActionSource};
@ -670,9 +670,9 @@ export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({
templateHtml: widgetTypeEntity.descriptor.templateHtml,
templateCss: widgetTypeEntity.descriptor.templateCss,
controllerScript: widgetTypeEntity.descriptor.controllerScript,
settingsSchema: widgetTypeEntity.descriptor.settingsSchema,
dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema,
latestDataKeySettingsSchema: widgetTypeEntity.descriptor.latestDataKeySettingsSchema,
settingsForm: widgetTypeEntity.descriptor.settingsForm,
dataKeySettingsForm: widgetTypeEntity.descriptor.dataKeySettingsForm,
latestDataKeySettingsForm: widgetTypeEntity.descriptor.latestDataKeySettingsForm,
settingsDirective: widgetTypeEntity.descriptor.settingsDirective,
dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective,
@ -699,9 +699,9 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
templateHtml: widgetInfo.templateHtml,
templateCss: widgetInfo.templateCss,
controllerScript: widgetInfo.controllerScript,
settingsSchema: widgetInfo.settingsSchema,
dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema,
latestDataKeySettingsSchema: widgetInfo.latestDataKeySettingsSchema,
settingsForm: widgetInfo.settingsForm,
dataKeySettingsForm: widgetInfo.dataKeySettingsForm,
latestDataKeySettingsForm: widgetInfo.latestDataKeySettingsForm,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,

18
ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html

@ -148,17 +148,15 @@
</mat-expansion-panel>
</mat-accordion>
</div>
<tb-datetime
dateText="entity-view.start-date"
timeText="entity-view.start-ts"
[maxDate]="maxStartTimeMs | async"
formControlName="startTimeMs"
<tb-datetime class="block"
dateText="{{ 'entity-view.start-ts' | translate }}"
[maxDate]="maxStartTimeMs | async"
formControlName="startTimeMs"
></tb-datetime>
<tb-datetime
dateText="entity-view.end-date"
timeText="entity-view.end-ts"
[minDate]="minEndTimeMs | async"
formControlName="endTimeMs"
<tb-datetime class="block"
dateText="{{ 'entity-view.end-ts' | translate }}"
[minDate]="minEndTimeMs | async"
formControlName="endTimeMs"
></tb-datetime>
<div formGroupName="additionalInfo">
<mat-form-field class="mat-block">

1
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)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
[class.tb-draggable]="!disabled"
[class.tb-draggable-form-table-row]="!disabled"
*ngFor="let behaviorControl of behaviorsFormArray().controls; trackBy: trackByBehavior; let $index = index;">
<tb-scada-symbol-metadata-behavior-row class="flex-1"

4
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-behaviors.component.ts

@ -177,8 +177,8 @@ export class ScadaSymbolBehaviorsComponent implements ControlValueAccessor, OnIn
behaviorDrop(event: CdkDragDrop<string[]>) {
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 {

14
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-components.module.ts

@ -35,15 +35,6 @@ import {
import {
ScadaSymbolBehaviorPanelComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-behavior-panel.component';
import {
ScadaSymbolPropertiesComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-properties.component';
import {
ScadaSymbolPropertyRowComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component';
import {
ScadaSymbolPropertyPanelComponent
} from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component';
import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings/common/widget-settings-common.module';
import {
ScadaSymbolMetadataTagFunctionPanelComponent
@ -58,10 +49,7 @@ import {
ScadaSymbolMetadataTagFunctionPanelComponent,
ScadaSymbolBehaviorsComponent,
ScadaSymbolBehaviorRowComponent,
ScadaSymbolBehaviorPanelComponent,
ScadaSymbolPropertiesComponent,
ScadaSymbolPropertyRowComponent,
ScadaSymbolPropertyPanelComponent
ScadaSymbolBehaviorPanelComponent
],
imports: [
CommonModule,

6
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html

@ -106,9 +106,11 @@
</tb-scada-symbol-metadata-behaviors>
</div>
<div [class.!hidden]="selectedOption !== 'properties'" class="mat-content overflow-hidden">
<tb-scada-symbol-metadata-properties
<tb-dynamic-form-properties
importExport
[exportFileName]="(modelValue?.title || 'scada-symbol') + '-properties'"
formControlName="properties">
</tb-scada-symbol-metadata-properties>
</tb-dynamic-form-properties>
</div>
</div>
</div>

2
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts

@ -92,7 +92,7 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni
@Input()
tags: string[];
private modelValue: ScadaSymbolMetadata;
modelValue: ScadaSymbolMetadata;
private propagateChange = null;

190
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.html

@ -1,190 +0,0 @@
<!--
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.
-->
<div class="tb-scada-symbol-property-settings-panel" [formGroup]="propertyFormGroup">
<div class="tb-scada-symbol-property-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-scada-symbol-property-settings-panel-content">
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.id</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="id" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.name</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input required matInput formControlName="name" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.type</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of scadaSymbolPropertyTypes" [value]="type">
{{ scadaSymbolPropertyTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.default-value</div>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.text"
class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="default" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.color"
asBoxInput
colorClearButton
formControlName="default">
</tb-color-input>
<tb-color-settings *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.color_settings"
formControlName="default"
settingsKey="{{ propertyFormGroup.get('id').value }}">
</tb-color-settings>
<mat-form-field *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.number"
appearance="outline"
class="number"
subscriptSizing="dynamic">
<input matInput formControlName="default"
[min]="propertyFormGroup.get('min').value"
[max]="propertyFormGroup.get('max').value"
[step]="propertyFormGroup.get('step').value"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.font"
formControlName="default"
clearButton
disabledLineHeight>
</tb-font-settings>
<tb-unit-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.units"
formControlName="default"></tb-unit-input>
<tb-value-input *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.switch"
[valueType]="ValueType.BOOLEAN"
formControlName="default"></tb-value-input>
<tb-material-icon-select *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.icon"
asBoxInput
formControlName="default">
</tb-material-icon-select>
</div>
<div *ngIf="propertyFormGroup.get('type').value === ScadaSymbolPropertyType.number" class="tb-form-row space-between">
<div translate>scada.property.number-settings</div>
<div class="flex flex-1 flex-row items-center justify-end gap-2">
<div class="tb-small-label" translate>scada.property.min</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="min"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>scada.property.max</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="max"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-divider vertical></mat-divider>
<div class="tb-small-label" translate>scada.property.step</div>
<mat-form-field appearance="outline" class="number flex" subscriptSizing="dynamic">
<input matInput formControlName="step" min="0"
type="number" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="required">
{{ 'scada.property.value-required' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
{{ 'scada.property.advanced-ui-settings' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.sub-label</div>
<mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="subLabel" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="divider">
{{ 'scada.property.vertical-divider-after' | translate }}
</mat-slide-toggle>
</div>
<div *ngIf="[ScadaSymbolPropertyType.number, ScadaSymbolPropertyType.text].includes(propertyFormGroup.get('type').value)"
class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.input-field-suffix</div>
<mat-form-field class="flex"
appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="fieldSuffix" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>scada.property.disable-on-property</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="disableOnProperty" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="null"></mat-option>
<mat-option *ngFor="let prop of booleanPropertyIds" [value]="prop">
{{ prop }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.property-row-classes</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="rowClass" multiple placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let clazz of scadaSymbolPropertyRowClasses" [value]="clazz">
{{ clazz }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="propertyFormGroup.get('type').value !== ScadaSymbolPropertyType.switch" class="tb-form-row">
<div class="fixed-title-width" translate>scada.property.property-field-classes</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="fieldClass" multiple placeholder="{{ 'widget-config.set' | translate }}">
<mat-option *ngFor="let clazz of scadaSymbolPropertyFieldClasses" [value]="clazz">
{{ clazz }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>
<div class="tb-scada-symbol-property-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ (disabled ? 'action.close' : 'action.cancel') | translate }}
</button>
<button *ngIf="!disabled"
mat-raised-button
color="primary"
type="button"
(click)="applyPropertySettings()"
[disabled]="propertyFormGroup.invalid || !propertyFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.apply') | translate }}
</button>
</div>
</div>

140
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.ts

@ -1,140 +0,0 @@
///
/// 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, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import {
ScadaSymbolProperty,
scadaSymbolPropertyFieldClasses,
scadaSymbolPropertyRowClasses,
ScadaSymbolPropertyType,
scadaSymbolPropertyTypes,
scadaSymbolPropertyTypeTranslations
} from '@home/components/widget/lib/scada/scada-symbol.models';
import { defaultPropertyValue } from '@home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component';
import { ValueType } from '@shared/models/constants';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@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;
ScadaSymbolPropertyType = ScadaSymbolPropertyType;
scadaSymbolPropertyTypes = scadaSymbolPropertyTypes;
scadaSymbolPropertyTypeTranslations = scadaSymbolPropertyTypeTranslations;
scadaSymbolPropertyRowClasses = scadaSymbolPropertyRowClasses;
scadaSymbolPropertyFieldClasses = scadaSymbolPropertyFieldClasses;
@Input()
isAdd = false;
@Input()
property: ScadaSymbolProperty;
@Input()
booleanPropertyIds: string[];
@Input()
disabled: boolean;
@Input()
popover: TbPopoverComponent<ScadaSymbolPropertyPanelComponent>;
@Output()
propertySettingsApplied = new EventEmitter<ScadaSymbolProperty>();
panelTitle: string;
propertyFormGroup: UntypedFormGroup;
private propertyType: ScadaSymbolPropertyType;
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
}
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)]]
}
);
if (this.disabled) {
this.propertyFormGroup.disable({emitEvent: false});
} else {
this.propertyFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).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: ScadaSymbolPropertyType = this.propertyFormGroup.get('type').value;
if (type === ScadaSymbolPropertyType.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;
}
}
}

43
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts

@ -27,14 +27,13 @@ import {
ScadaSymbolBehavior,
ScadaSymbolBehaviorType,
scadaSymbolContentData,
ScadaSymbolMetadata,
ScadaSymbolProperty,
ScadaSymbolPropertyType
ScadaSymbolMetadata
} from '@home/components/widget/lib/scada/scada-symbol.models';
import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { AceHighlightRule, AceHighlightRules } from '@shared/models/ace/ace.models';
import { HelpLinks, ValueType } from '@shared/models/constants';
import { formPropertyCompletions } from '@shared/models/dynamic-form.models';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
import ITooltipsterHelper = JQueryTooltipster.ITooltipsterHelper;
@ -1141,11 +1140,8 @@ export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags
meta: 'object',
type: 'object',
description: 'An object holding all defined SCADA symbol properties.',
children: {}
children: formPropertyCompletions(metadata.properties, customTranslate)
};
for (const property of metadata.properties) {
properties.children[property.id] = scadaSymbolPropertyCompletion(property, customTranslate);
}
const values: TbEditorCompletion = {
meta: 'object',
type: 'object',
@ -1441,18 +1437,6 @@ export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags
};
};
const scadaSymbolPropertyCompletion = (property: ScadaSymbolProperty, customTranslate: CustomTranslatePipe): TbEditorCompletion => {
let description = customTranslate.transform(property.name, property.name);
if (property.subLabel) {
description += ` <small>${customTranslate.transform(property.subLabel, property.subLabel)}</small>`;
}
return {
meta: 'property',
description,
type: scadaSymbolPropertyCompletionType(property.type)
};
};
const scadaSymbolValueCompletion = (value: ScadaSymbolBehavior, customTranslate: CustomTranslatePipe): TbEditorCompletion => {
const description = customTranslate.transform(value.name, value.name);
return {
@ -1462,27 +1446,6 @@ const scadaSymbolValueCompletion = (value: ScadaSymbolBehavior, customTranslate:
};
};
const scadaSymbolPropertyCompletionType = (type: ScadaSymbolPropertyType): string => {
switch (type) {
case ScadaSymbolPropertyType.text:
return 'string';
case ScadaSymbolPropertyType.number:
return 'number';
case ScadaSymbolPropertyType.switch:
return 'boolean';
case ScadaSymbolPropertyType.color:
return 'color string';
case ScadaSymbolPropertyType.color_settings:
return 'ColorProcessor';
case ScadaSymbolPropertyType.font:
return 'Font';
case ScadaSymbolPropertyType.units:
return 'units string';
case ScadaSymbolPropertyType.icon:
return 'icon string';
}
};
const scadaSymbolValueCompletionType = (type: ValueType): string => {
switch (type) {
case ValueType.STRING:

69
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -188,52 +188,37 @@
</div>
<div #topRightPanel class="tb-split tb-content">
<mat-tab-group mat-stretch-tabs="false" dynamicHeight="true" style="width: 100%; height: 100%;">
<mat-tab label="{{ 'widget.settings-schema' | translate }}">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonSettingsFullscreen">
<div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyJson()">
{{ 'widget.tidy' | translate }}
</button>
<button mat-icon-button class="tb-mat-32"
(click)="jsonSettingsFullscreen = !jsonSettingsFullscreen"
matTooltip="{{(jsonSettingsFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ jsonSettingsFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div #settingsJsonInput></div>
<mat-tab label="{{ 'widget.settings-form' | translate }}">
<div class="tb-resize-container" style="background-color: #fff;">
<tb-dynamic-form-properties
importExport
[exportFileName]="(widget.widgetName || 'widget') + '-settings-form'"
[(ngModel)]="widget.settingsForm"
(ngModelChange)="settingsFormUpdated()"
noBorder fillHeight>
</tb-dynamic-form-properties>
</div>
</mat-tab>
<mat-tab label="{{ 'widget.datakey-settings-schema' | translate }}">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonDataKeySettingsFullscreen">
<div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyDataKeyJson()">
{{ 'widget.tidy' | translate }}
</button>
<button mat-icon-button class="tb-mat-32"
(click)="jsonDataKeySettingsFullscreen = !jsonDataKeySettingsFullscreen"
matTooltip="{{(jsonDataKeySettingsFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ jsonDataKeySettingsFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div #dataKeySettingsJsonInput></div>
<mat-tab label="{{ 'widget.data-key-settings-form' | translate }}">
<div class="tb-resize-container" style="background-color: #fff;">
<tb-dynamic-form-properties
importExport
[exportFileName]="(widget.widgetName || 'widget') + '-data-key-settings-form'"
[(ngModel)]="widget.dataKeySettingsForm"
(ngModelChange)="dataKeySettingsFormUpdated()"
noBorder fillHeight>
</tb-dynamic-form-properties>
</div>
</mat-tab>
<mat-tab [aria-labelledby]="widget.type !== widgetTypes.timeseries ? 'hidden': ''" label="{{ 'widget.latest-datakey-settings-schema' | translate }}">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonLatestDataKeySettingsFullscreen">
<div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyLatestDataKeyJson()">
{{ 'widget.tidy' | translate }}
</button>
<button mat-icon-button class="tb-mat-32"
(click)="jsonLatestDataKeySettingsFullscreen = !jsonLatestDataKeySettingsFullscreen"
matTooltip="{{(jsonLatestDataKeySettingsFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ jsonLatestDataKeySettingsFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div #latestDataKeySettingsJsonInput></div>
<mat-tab [aria-labelledby]="widget.type !== widgetTypes.timeseries ? 'hidden': ''" label="{{ 'widget.latest-data-key-settings-form' | translate }}">
<div class="tb-resize-container" style="background-color: #fff;">
<tb-dynamic-form-properties
importExport
[exportFileName]="(widget.widgetName || 'widget') + '-latest-data-key-settings-form'"
[(ngModel)]="widget.latestDataKeySettingsForm"
(ngModelChange)="latestDataKeySettingsFormUpdated()"
noBorder fillHeight>
</tb-dynamic-form-properties>
</div>
</mat-tab>
<mat-tab label="{{ 'widget.widget-settings' | translate }}">

135
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts

@ -21,8 +21,10 @@ import {
EventEmitter,
Inject,
OnDestroy,
OnInit, Renderer2,
ViewChild, ViewContainerRef,
OnInit,
Renderer2,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { Store } from '@ngrx/store';
@ -30,6 +32,7 @@ import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service';
import { detailsToWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import {
migrateWidgetTypeToDynamicForms,
TargetDeviceType,
Widget,
WidgetConfig,
@ -65,12 +68,13 @@ import { Observable } from 'rxjs/internal/Observable';
import { catchError, map, tap } from 'rxjs/operators';
import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models';
import { HttpClient, HttpStatusCode } from '@angular/common/http';
import Timeout = NodeJS.Timeout;
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { loadModulesCompleter } from '@shared/models/js-function.models';
import { TbPopoverService } from '@shared/components/popover.service';
import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component';
import { MatIconButton } from '@angular/material/button';
import { formPropertyCompletions } from '@shared/models/dynamic-form.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import Timeout = NodeJS.Timeout;
// @dynamic
@Component({
@ -105,15 +109,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
@ViewChild('cssInput', {static: true})
cssInputElmRef: ElementRef;
@ViewChild('settingsJsonInput', {static: true})
settingsJsonInputElmRef: ElementRef;
@ViewChild('dataKeySettingsJsonInput', {static: true})
dataKeySettingsJsonInputElmRef: ElementRef;
@ViewChild('latestDataKeySettingsJsonInput', {static: true})
latestDataKeySettingsJsonInputElmRef: ElementRef;
@ViewChild('javascriptInput', {static: true})
javascriptInputElmRef: ElementRef;
@ -151,9 +146,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
fullscreen = false;
htmlFullscreen = false;
cssFullscreen = false;
jsonSettingsFullscreen = false;
jsonDataKeySettingsFullscreen = false;
jsonLatestDataKeySettingsFullscreen = false;
javascriptFullscreen = false;
iFrameFullscreen = false;
@ -161,9 +153,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {};
htmlEditor: Ace.Editor;
cssEditor: Ace.Editor;
jsonSettingsEditor: Ace.Editor;
dataKeyJsonSettingsEditor: Ace.Editor;
latestDataKeyJsonSettingsEditor: Ace.Editor;
jsEditor: Ace.Editor;
private initialCompleters: Ace.Completer[];
aceResize$: ResizeObserver;
@ -197,6 +186,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private customTranslate: CustomTranslatePipe,
private http: HttpClient) {
super(store);
@ -353,45 +343,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
})
));
editorsObservables.push(this.createAceEditor(this.settingsJsonInputElmRef, 'json').pipe(
tap((editor) => {
this.jsonSettingsEditor = editor;
this.jsonSettingsEditor.on('input', () => {
const editorValue = this.jsonSettingsEditor.getValue();
if (this.widget.settingsSchema !== editorValue) {
this.widget.settingsSchema = editorValue;
this.isDirty = true;
}
});
})
));
editorsObservables.push(this.createAceEditor(this.dataKeySettingsJsonInputElmRef, 'json').pipe(
tap((editor) => {
this.dataKeyJsonSettingsEditor = editor;
this.dataKeyJsonSettingsEditor.on('input', () => {
const editorValue = this.dataKeyJsonSettingsEditor.getValue();
if (this.widget.dataKeySettingsSchema !== editorValue) {
this.widget.dataKeySettingsSchema = editorValue;
this.isDirty = true;
}
});
})
));
editorsObservables.push(this.createAceEditor(this.latestDataKeySettingsJsonInputElmRef, 'json').pipe(
tap((editor) => {
this.latestDataKeyJsonSettingsEditor = editor;
this.latestDataKeyJsonSettingsEditor.on('input', () => {
const editorValue = this.latestDataKeyJsonSettingsEditor.getValue();
if (this.widget.latestDataKeySettingsSchema !== editorValue) {
this.widget.latestDataKeySettingsSchema = editorValue;
this.isDirty = true;
}
});
})
));
editorsObservables.push(this.createAceEditor(this.javascriptInputElmRef, 'javascript').pipe(
tap((editor) => {
this.jsEditor = editor;
@ -405,6 +356,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.jsEditor.on('change', () => {
this.cleanupJsErrors();
});
if (!(this.jsEditor as any).completer) {
this.jsEditor.execCommand("startAutocomplete");
(this.jsEditor as any).completer.detach();
}
(this.jsEditor as any).completer.popup.container.style.width = '500px';
this.initialCompleters = this.jsEditor.completers || [];
})
));
@ -420,10 +376,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
private setAceEditorValues() {
this.htmlEditor.setValue(this.widget.templateHtml ? this.widget.templateHtml : '', -1);
this.cssEditor.setValue(this.widget.templateCss ? this.widget.templateCss : '', -1);
this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1);
this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1);
this.latestDataKeyJsonSettingsEditor.setValue(this.widget.latestDataKeySettingsSchema ?
this.widget.latestDataKeySettingsSchema : '', -1);
this.jsEditor.setValue(this.controllerScriptBody ? this.controllerScriptBody : '', -1);
this.updateControllerScriptCompleters();
}
@ -596,7 +548,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}),
catchError((err) => {
if (id && err.status === HttpStatusCode.Conflict) {
return this.widgetService.getWidgetTypeById(id.id);
return this.widgetService.getWidgetTypeById(id.id).pipe(
map((details) => {
return migrateWidgetTypeToDynamicForms(details);
})
);
}
return throwError(() => err);
}),
@ -754,43 +710,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
);
}
beautifyJson(): void {
beautifyJs(this.widget.settingsSchema, {indent_size: 4}).subscribe(
(res) => {
if (this.widget.settingsSchema !== res) {
this.isDirty = true;
this.widget.settingsSchema = res;
this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1);
}
}
);
}
beautifyDataKeyJson(): void {
beautifyJs(this.widget.dataKeySettingsSchema, {indent_size: 4}).subscribe(
(res) => {
if (this.widget.dataKeySettingsSchema !== res) {
this.isDirty = true;
this.widget.dataKeySettingsSchema = res;
this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1);
}
}
);
}
beautifyLatestDataKeyJson(): void {
beautifyJs(this.widget.latestDataKeySettingsSchema, {indent_size: 4}).subscribe(
(res) => {
if (this.widget.latestDataKeySettingsSchema !== res) {
this.isDirty = true;
this.widget.latestDataKeySettingsSchema = res;
this.latestDataKeyJsonSettingsEditor.setValue(this.widget.latestDataKeySettingsSchema ?
this.widget.latestDataKeySettingsSchema : '', -1);
}
}
);
}
beautifyJs(): void {
beautifyJs(this.controllerScriptBody, {indent_size: 4, wrap_line_length: 60}).subscribe(
(res) => {
@ -873,6 +792,19 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.isDirty = true;
}
settingsFormUpdated() {
this.isDirty = true;
this.updateControllerScriptCompleters();
}
dataKeySettingsFormUpdated() {
this.isDirty = true;
}
latestDataKeySettingsFormUpdated() {
this.isDirty = true;
}
editControllerScriptModules($event: Event, button: MatIconButton) {
if ($event) {
$event.stopPropagation();
@ -951,7 +883,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
const modulesCompleterObservable = loadModulesCompleter(this.http, this.controllerScriptModules);
modulesCompleterObservable.subscribe((modulesCompleter) => {
const completers: Ace.Completer[] = [];
completers.push(widgetEditorCompleter);
const formPropertiesCompletions = formPropertyCompletions(this.widget.settingsForm || [], this.customTranslate);
completers.push(widgetEditorCompleter(formPropertiesCompletions));
if (modulesCompleter) {
completers.push(modulesCompleter);
}

161
ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts

@ -15,77 +15,100 @@
///
import { TbEditorCompleter, TbEditorCompletions } from '@shared/models/ace/completion.models';
import { widgetContextCompletions } from '@shared/models/ace/widget-completion.models';
import {
widgetContextCompletions,
widgetContextCompletionsWithSettings
} from '@shared/models/ace/widget-completion.models';
import { serviceCompletions } from '@shared/models/ace/service-completion.models';
const widgetEditorCompletions: TbEditorCompletions = {
... {self: {
description: 'Built-in variable <b>self</b> that is a reference to the widget instance',
type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L350" target="_blank">WidgetTypeInstance</a>',
meta: 'object',
children: {
...{
onInit: {
description: 'The first function which is called when widget is ready for initialization.<br>Should be used to prepare widget DOM, process widget settings and initial subscription information.',
meta: 'function'
},
onDataUpdated: {
description: 'Called when the new data is available from the widget subscription.<br>Latest data can be accessed from ' +
'the <code>defaultSubscription</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
meta: 'function'
},
onResize: {
description: 'Called when widget container is resized. Latest <code>width</code> and <code>height</code> can be obtained from <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
meta: 'function'
},
onEditModeChanged: {
description: 'Called when dashboard editing mode is changed. Latest mode is handled by <code>isEdit</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
meta: 'function'
},
onMobileModeChanged: {
description: 'Called when dashboard view width crosses mobile breakpoint. Latest state is handled by <code>isMobile</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
meta: 'function'
},
onDestroy: {
description: 'Called when widget element is destroyed. Should be used to cleanup all resources if necessary.',
meta: 'function'
},
getSettingsSchema: {
description: 'Optional function returning widget settings schema json as alternative to <b>Settings tab</b> of <a href="https://thingsboard.io/docs/user-guide/contribution/widgets-development/#settings-schema-section" target="_blank">Settings schema section</a>.',
meta: 'function',
return: {
description: 'An widget settings schema json',
type: 'object'
}
},
getDataKeySettingsSchema: {
description: 'Optional function returning particular data key settings schema json as alternative to <b>Data key settings schema</b> of <a href="https://thingsboard.io/docs/user-guide/contribution/widgets-development/#settings-schema-section" target="_blank">Settings schema section</a>.',
meta: 'function',
return: {
description: 'A particular data key settings schema json',
type: 'object'
}
},
typeParameters: {
description: 'Returns object describing widget datasource parameters.',
meta: 'function',
return: {
description: 'An object describing widget datasource parameters.',
type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L146" target="_blank">WidgetTypeParameters</a>'
}
},
actionSources: {
description: 'Returns map describing available widget action sources used to define user actions.',
meta: 'function',
return: {
description: 'A map of action sources by action source id.',
type: '{[actionSourceId: string]: <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L118" target="_blank">WidgetActionSource</a>}'
}
const widgetEditorCompletions = (settingsCompletions?: TbEditorCompletions): TbEditorCompletions => {
return {
... {self: {
description: 'Built-in variable <b>self</b> that is a reference to the widget instance',
type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L350" target="_blank">WidgetTypeInstance</a>',
meta: 'object',
children: {
...{
onInit: {
description: 'The first function which is called when widget is ready for initialization.<br>Should be used to prepare widget DOM, process widget settings and initial subscription information.',
meta: 'function'
},
onDataUpdated: {
description: 'Called when the new data is available from the widget subscription.<br>Latest data can be accessed from ' +
'the <code>defaultSubscription</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
meta: 'function'
},
onResize: {
description: 'Called when widget container is resized. Latest <code>width</code> and <code>height</code> can be obtained from <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
meta: 'function'
},
onEditModeChanged: {
description: 'Called when dashboard editing mode is changed. Latest mode is handled by <code>isEdit</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
meta: 'function'
},
onMobileModeChanged: {
description: 'Called when dashboard view width crosses mobile breakpoint. Latest state is handled by <code>isMobile</code> property of <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L83" target="_blank">widget context (<code>ctx</code>)</a>.',
meta: 'function'
},
onDestroy: {
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 <b>Settings form</b> tab of settings section.',
meta: 'function',
return: {
description: 'An array of widget settings form properties',
type: 'Array&lt;FormProperty&gt;'
}
},
getDataKeySettingsForm: {
description: 'Optional function returning particular data key settings form array as alternative to <b>Data key settings form</b> tab of settings section.',
meta: 'function',
return: {
description: 'An array of data key settings form properties',
type: 'Array&lt;FormProperty&gt;'
}
},
getSettingsSchema: {
description: '<b>Deprecated</b>. Use getSettingsForm() function.',
meta: 'function',
return: {
description: 'An widget settings schema json',
type: 'object'
}
},
getDataKeySettingsSchema: {
description: '<b>Deprecated</b>. Use getDataKeySettingsForm() function.',
meta: 'function',
return: {
description: 'A particular data key settings schema json',
type: 'object'
}
},
typeParameters: {
description: 'Returns object describing widget datasource parameters.',
meta: 'function',
return: {
description: 'An object describing widget datasource parameters.',
type: '<a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L146" target="_blank">WidgetTypeParameters</a>'
}
},
actionSources: {
description: 'Returns map describing available widget action sources used to define user actions.',
meta: 'function',
return: {
description: 'A map of action sources by action source id.',
type: '{[actionSourceId: string]: <a href="https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L118" target="_blank">WidgetActionSource</a>}'
}
}
},
...widgetContextCompletionsWithSettings(settingsCompletions)
}
},
...widgetContextCompletions
}
}}
}}
}
};
export const widgetEditorCompleter = new TbEditorCompleter(widgetEditorCompletions);
export const widgetEditorCompleter = (settingsCompletions?: TbEditorCompletions): TbEditorCompleter => {
return new TbEditorCompleter(widgetEditorCompletions(settingsCompletions));
}

18
ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts

@ -26,7 +26,12 @@ import { WidgetService } from '@core/http/widget.service';
import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
import { map } from 'rxjs/operators';
import { detailsToWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { widgetType, WidgetTypeDetails, WidgetTypeInfo } from '@app/shared/models/widget.models';
import {
migrateWidgetTypeToDynamicForms,
widgetType,
WidgetTypeDetails,
WidgetTypeInfo
} from '@app/shared/models/widget.models';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { WidgetTypesTableConfigResolver } from '@home/pages/widget/widget-types-table-config.resolver';
@ -68,10 +73,13 @@ const widgetEditorDataResolver: ResolveFn<WidgetEditorData> = (route: ActivatedR
);
} else {
return inject(WidgetService).getWidgetTypeById(widgetTypeId).pipe(
map((result) => ({
widgetTypeDetails: result,
widget: detailsToWidgetInfo(result)
}))
map((result) => {
result = migrateWidgetTypeToDynamicForms(result);
return {
widgetTypeDetails: result,
widget: detailsToWidgetInfo(result)
};
})
);
}
};

2
ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts

@ -29,6 +29,7 @@ import { WidgetTypeTabsComponent } from '@home/pages/widget/widget-type-tabs.com
import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component';
import { WidgetTypeAutocompleteComponent } from '@home/pages/widget/widget-type-autocomplete.component';
import { WidgetsBundleDialogComponent } from '@home/pages/widget/widgets-bundle-dialog.component';
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
@NgModule({
declarations: [
@ -47,6 +48,7 @@ import { WidgetsBundleDialogComponent } from '@home/pages/widget/widgets-bundle-
CommonModule,
SharedModule,
HomeComponentsModule,
WidgetConfigComponentsModule,
WidgetLibraryRoutingModule
]
})

10
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<string>;
@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 ? '' : ' {'}`;

23
ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts

@ -1,23 +0,0 @@
///
/// 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 { JsonSettingsSchema } from '@shared/models/widget.models';
export interface JsonFormComponentData extends JsonSettingsSchema {
model?: any;
settingsDirective?: string;
}

23
ui-ngx/src/app/shared/components/json-form/json-form.component.html

@ -1,23 +0,0 @@
<!--
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.
-->
<div class="tb-json-form" style="background: #fff;" tb-fullscreen [fullscreenElement]="reactFullscreen"
[fullscreen]="isFullscreen"
(fullscreenChanged)="onFullscreenChanged($event)">
<div #reactRoot></div>
<div class="tb-json-form" #reactFullscreen></div>
</div>

293
ui-ngx/src/app/shared/components/json-form/json-form.component.ts

@ -1,293 +0,0 @@
///
/// 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,
forwardRef,
Input,
OnChanges,
OnDestroy,
Renderer2,
SimpleChanges,
ViewChild, ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { deepClone, isString, unwrapModule } from '@app/core/utils';
import { JsonFormProps } from './react/json-form.models';
import inspector from 'schema-inspector';
import tinycolor from 'tinycolor2';
import { DialogService } from '@app/core/services/dialog.service';
import JsonFormUtils from './react/json-form-utils';
import { JsonFormComponentData } from './json-form-component.models';
import { GroupInfo } from '@shared/models/widget.models';
import { Observable } from 'rxjs/internal/Observable';
import { forkJoin, from } from 'rxjs';
import { TbPopoverService } from '@shared/components/popover.service';
@Component({
selector: 'tb-json-form',
templateUrl: './json-form.component.html',
styleUrls: ['./json-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => JsonFormComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => JsonFormComponent),
multi: true,
}
],
encapsulation: ViewEncapsulation.None
})
export class JsonFormComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy {
@ViewChild('reactRoot', {static: true})
reactRootElmRef: ElementRef<HTMLElement>;
@ViewChild('reactFullscreen', {static: true})
reactFullscreenElmRef: ElementRef<HTMLElement>;
private readonlyValue: boolean;
get readonly(): boolean {
return this.readonlyValue;
}
@Input()
set required(value: boolean) {
this.readonlyValue = coerceBooleanProperty(value);
}
formProps: JsonFormProps = {
isFullscreen: false,
option: {
formDefaults: {
startEmpty: true
}
},
onModelChange: this.onModelChange.bind(this),
onColorClick: this.onColorClick.bind(this),
onIconClick: this.onIconClick.bind(this),
onToggleFullscreen: this.onToggleFullscreen.bind(this),
onHelpClick: this.onHelpClick.bind(this)
};
data: JsonFormComponentData;
model: any;
schema: any;
form: any;
groupInfoes: GroupInfo[];
isModelValid = true;
isFullscreen = false;
fullscreenFinishFn: (el: Element) => void;
private reactRoot: any;
private propagateChange = null;
private propagateChangePending = false;
private writingValue = false;
private updateViewPending = false;
constructor(public elementRef: ElementRef,
private dialogs: DialogService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
protected store: Store<AppState>,
private cd: ChangeDetectorRef) {
}
ngOnDestroy(): void {
this.destroyReactSchemaForm();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
if (this.propagateChangePending) {
this.propagateChangePending = false;
setTimeout(() => {
this.propagateChange(this.data);
}, 0);
}
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
}
public validate(c: UntypedFormControl) {
return this.isModelValid ? null : {
modelValid: false
};
}
writeValue(data: JsonFormComponentData): void {
this.writingValue = true;
this.data = data;
this.schema = this.data && this.data.schema ? deepClone(this.data.schema) : {
type: 'object'
};
this.schema.strict = true;
this.form = this.data && this.data.form ? deepClone(this.data.form) : [ '*' ];
this.groupInfoes = this.data && this.data.groupInfoes ? deepClone(this.data.groupInfoes) : [];
this.model = this.data && this.data.model || {};
this.model = inspector.sanitize(this.schema, this.model).data;
this.updateAndRender();
this.isModelValid = this.validateModel();
this.writingValue = false;
if (!this.isModelValid || this.updateViewPending) {
this.updateView();
}
}
updateView() {
if (!this.writingValue) {
this.updateViewPending = false;
if (this.data) {
this.data.model = this.model;
if (this.propagateChange) {
try {
this.propagateChange(this.data);
} catch (e) {
this.propagateChangePending = true;
}
} else {
this.propagateChangePending = true;
}
}
} else {
this.updateViewPending = true;
}
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'readonly') {
this.updateAndRender();
}
}
}
}
private onModelChange(key: (string | number)[], val: any, forceUpdate = false) {
if (isString(val) && val === '') {
val = undefined;
}
if (JsonFormUtils.updateValue(key, this.model, val) || forceUpdate) {
this.isModelValid = this.validateModel();
this.updateView();
}
}
private onColorClick(key: (string | number)[],
val: tinycolor.ColorFormats.RGBA,
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) {
this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((result) => {
if (!result?.canceled && colorSelectedFn) {
colorSelectedFn(tinycolor(result?.color).toRgb());
}
});
}
private onIconClick(key: (string | number)[],
val: string,
iconSelectedFn: (icon: string) => void) {
this.dialogs.materialIconPicker(val).subscribe((result) => {
if (!result?.canceled && iconSelectedFn) {
iconSelectedFn(result?.icon);
}
});
}
private onToggleFullscreen(fullscreenFinishFn?: (el: Element) => void) {
this.isFullscreen = !this.isFullscreen;
this.fullscreenFinishFn = fullscreenFinishFn;
this.cd.markForCheck();
}
onFullscreenChanged(fullscreen: boolean) {
this.formProps.isFullscreen = fullscreen;
this.renderReactSchemaForm(false);
if (this.fullscreenFinishFn) {
this.fullscreenFinishFn(this.reactFullscreenElmRef.nativeElement);
this.fullscreenFinishFn = null;
}
}
private onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) {
const trigger = event.currentTarget as Element;
this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef, helpId, '', '', null, helpVisibleFn, helpReadyFn);
}
private updateAndRender() {
this.formProps.option.formDefaults.readonly = this.readonly;
this.formProps.schema = this.schema;
this.formProps.form = this.form;
this.formProps.groupInfoes = this.groupInfoes;
this.formProps.model = this.model;
this.renderReactSchemaForm();
}
private renderReactSchemaForm(destroy: boolean = true) {
if (destroy) {
this.destroyReactSchemaForm();
}
// import ReactSchemaForm from './react/json-form-react';
const reactSchemaFormObservables: Observable<any>[] = [
from(import('react')),
from(import('react-dom')),
from(import('react-dom/client')),
from(import('./react/json-form-react'))
];
forkJoin(reactSchemaFormObservables).subscribe(
(modules) => {
const react = unwrapModule(modules[0]);
const reactDomClient = unwrapModule(modules[2]);
const jsonFormReact = unwrapModule(modules[3]);
this.reactRoot = reactDomClient.createRoot(this.reactRootElmRef.nativeElement);
this.reactRoot.render(react.createElement(jsonFormReact, this.formProps));
}
);
}
private destroyReactSchemaForm() {
this.reactRoot?.unmount();
}
private validateModel(): boolean {
if (this.schema && this.model) {
return JsonFormUtils.validateBySchema(this.schema, this.model).valid;
}
return true;
}
}

239
ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx

@ -1,239 +0,0 @@
/*
* 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 * as React from 'react';
import * as ReactDOM from 'react-dom';
import ThingsboardBaseComponent from './json-form-base-component';
import reactCSS from 'reactcss';
import Button from '@mui/material/Button';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import { IEditorProps } from 'react-ace/src/types';
import { map, mergeMap } from 'rxjs/operators';
import { getAce } from '@shared/models/ace/ace.models';
import { from, lastValueFrom } from 'rxjs';
import { Observable } from 'rxjs/internal/Observable';
import { CircularProgress, IconButton } from '@mui/material';
import { MouseEvent } from 'react';
import { Help, HelpOutline } from '@mui/icons-material';
import { unwrapModule } from '@core/utils';
const ReactAce = React.lazy(() => {
return lastValueFrom(getAce().pipe(
mergeMap(() => {
return from(import('react-ace')).pipe(
map((module) => unwrapModule(module)
));
})
));
});
interface ThingsboardAceEditorProps extends JsonFormFieldProps {
mode: string;
onTidy: (value: string) => Observable<string>;
}
interface ThingsboardAceEditorState extends JsonFormFieldState {
isFull: boolean;
fullscreenContainerElement: Element;
helpVisible: boolean;
helpReady: boolean;
focused: boolean;
}
class ThingsboardAceEditor extends React.Component<ThingsboardAceEditorProps, ThingsboardAceEditorState> {
private aceEditor: IEditorProps;
constructor(props: ThingsboardAceEditorProps) {
super(props);
this.onValueChanged = this.onValueChanged.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onTidy = this.onTidy.bind(this);
this.onHelp = this.onHelp.bind(this);
this.onLoad = this.onLoad.bind(this);
this.onToggleFull = this.onToggleFull.bind(this);
const value = props.value ? props.value + '' : '';
this.state = {
isFull: false,
fullscreenContainerElement: null,
helpVisible: false,
helpReady: true,
value,
focused: false
};
}
onValueChanged(value) {
this.setState({
value
});
this.props.onChangeValidate({
target: {
value
}
});
}
onBlur() {
this.setState({ focused: false });
}
onFocus() {
this.setState({ focused: true });
}
onTidy() {
if (!this.props.form.readonly) {
const value = this.state.value;
this.props.onTidy(value).subscribe(
(processedValue) => {
this.setState({
value: processedValue
});
this.props.onChangeValidate({
target: {
value: processedValue
}
});
}
);
}
}
onHelp(event: MouseEvent) {
if (this.state.helpVisible && !this.state.helpReady) {
event.preventDefault();
event.stopPropagation();
} else {
this.props.onHelpClick(event, this.props.form.helpId,
(visible) => {
this.setState({
helpVisible: visible
});
}, (ready) => {
this.setState({
helpReady: ready
});
});
}
}
onLoad(editor: IEditorProps) {
this.aceEditor = editor;
}
onToggleFull() {
this.props.onToggleFullscreen((el) => {
this.setState({ isFull: !this.state.isFull, fullscreenContainerElement: el });
});
}
componentDidUpdate() {
}
render() {
const styles = reactCSS({
default: {
tidyButtonStyle: {
color: '#7B7B7B',
minWidth: '32px',
minHeight: '15px',
lineHeight: '15px',
fontSize: '0.800rem',
margin: '0',
padding: '4px',
height: '23px',
borderRadius: '5px',
marginLeft: '5px'
}
}
});
let labelClass = 'tb-label';
if (this.props.form.required) {
labelClass += ' tb-required';
}
if (this.props.form.readonly) {
labelClass += ' tb-readonly';
}
if (this.state.focused) {
labelClass += ' tb-focused';
}
let containerClass = 'tb-container';
const style = this.props.form.style || {width: '100%'};
if (this.state.isFull) {
containerClass += ' fullscreen-form-field';
}
const formDom = (
<div className={containerClass}>
<label className={labelClass}>{this.props.form.title}</label>
<div className='json-form-ace-editor'>
<div className='title-panel'>
<label>{this.props.mode}</label>
{ this.props.onTidy ? <Button style={ styles.tidyButtonStyle }
className='tidy-button' onClick={this.onTidy}>Tidy</Button> : null }
{ this.props.form.helpId ? <div style={ {position: 'relative', display: 'inline-block', marginLeft: '5px'} }>
<IconButton color='primary'
className='help-button' onClick={this.onHelp}>
{this.state.helpVisible ? <Help /> : <HelpOutline /> }
</IconButton>
{ this.state.helpVisible && !this.state.helpReady ?
<div className='tb-absolute-fill help-button-loading'>
<CircularProgress size={18} thickness={4}/>
</div> : null }</div> : null }
<Button style={ styles.tidyButtonStyle }
className='tidy-button' onClick={this.onToggleFull}>
{this.state.isFull ?
'Exit fullscreen' : 'Fullscreen'}
</Button>
</div>
<React.Suspense fallback={<div>Loading...</div>}>
<ReactAce mode={this.props.mode}
theme={'textmate'}
height={this.state.isFull ? '100%' : '150px'}
width={this.state.isFull ? '100%' : '300px'}
onChange={this.onValueChanged}
onFocus={this.onFocus}
onBlur={this.onBlur}
onLoad={this.onLoad}
name={this.props.form.title}
value={this.state.value}
readOnly={this.props.form.readonly}
editorProps={{$blockScrolling: Infinity}}
enableBasicAutocompletion={true}
enableSnippets={true}
enableLiveAutocompletion={true}
style={style}/>
</React.Suspense>
</div>
<div className='json-form-error'
style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}</div>
</div>
);
if (this.state.isFull) {
return ReactDOM.createPortal(formDom, this.state.fullscreenContainerElement);
} else {
return (
<div>
{formDom}
</div>
);
}
}
}
export default ThingsboardBaseComponent(ThingsboardAceEditor);

177
ui-ngx/src/app/shared/components/json-form/react/json-form-array.tsx

@ -1,177 +0,0 @@
/*
* 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 * as React from 'react';
import JsonFormUtils from './json-form-utils';
import ThingsboardBaseComponent from './json-form-base-component';
import Button from '@mui/material/Button';
import _ from 'lodash';
import IconButton from '@mui/material/IconButton';
import Clear from '@mui/icons-material/Clear';
import Add from '@mui/icons-material/Add';
import Tooltip from '@mui/material/Tooltip';
import {
JsonFormData,
JsonFormFieldProps,
JsonFormFieldState
} from '@shared/components/json-form/react/json-form.models';
interface ThingsboardArrayState extends JsonFormFieldState {
model: any[];
keys: number[];
}
class ThingsboardArray extends React.Component<JsonFormFieldProps, ThingsboardArrayState> {
constructor(props: JsonFormFieldProps) {
super(props);
this.onAppend = this.onAppend.bind(this);
this.onDelete = this.onDelete.bind(this);
const model = JsonFormUtils.selectOrSet(this.props.form.key, this.props.model) || [];
const keys: number[] = [];
for (let i = 0; i < model.length; i++) {
keys.push(i);
}
this.state = {
model,
keys
};
}
componentDidMount() {
if (this.props.form.startEmpty !== true && this.state.model.length === 0) {
this.onAppend();
}
}
onAppend() {
let empty;
if (this.props.form && this.props.form.schema && this.props.form.schema.items) {
const items = this.props.form.schema.items;
if (items.type && items.type.indexOf('object') !== -1) {
empty = {};
if (!this.props.options || this.props.options.setSchemaDefaults !== false) {
empty = typeof items.default !== 'undefined' ? items.default : empty;
if (empty) {
JsonFormUtils.traverseSchema(items, (prop, path) => {
if (typeof prop.default !== 'undefined') {
JsonFormUtils.selectOrSet(path, empty, prop.default);
}
});
}
}
} else if (items.type && items.type.indexOf('array') !== -1) {
empty = [];
if (!this.props.options || this.props.options.setSchemaDefaults !== false) {
empty = items.default || empty;
}
} else {
if (!this.props.options || this.props.options.setSchemaDefaults !== false) {
empty = items.default || empty;
}
}
}
const newModel = this.state.model;
newModel.push(empty);
const newKeys = this.state.keys;
let key = 0;
if (newKeys.length > 0) {
key = newKeys[newKeys.length - 1] + 1;
}
newKeys.push(key);
this.setState({
model: newModel,
keys: newKeys
}
);
this.props.onChangeValidate(this.state.model, true);
}
onDelete(index: number) {
const newModel = this.state.model;
newModel.splice(index, 1);
const newKeys = this.state.keys;
newKeys.splice(index, 1);
this.setState(
{
model: newModel,
keys: newKeys
}
);
this.props.onChangeValidate(this.state.model, true);
}
setIndex(index: number) {
return (form: JsonFormData) => {
if (form.key) {
form.key[form.key.indexOf('')] = index;
}
};
}
copyWithIndex(form: JsonFormData, index: number): JsonFormData {
const copy: JsonFormData = _.cloneDeep(form);
copy.arrayIndex = index;
JsonFormUtils.traverseForm(copy, this.setIndex(index));
return copy;
}
render() {
const arrays = [];
const model = this.state.model;
const keys = this.state.keys;
for (let i = 0; i < model.length; i++ ) {
let removeButton: React.JSX.Element = null;
if (!this.props.form.readonly) {
const boundOnDelete = this.onDelete.bind(this, i);
removeButton = <Tooltip title='Remove'><IconButton onClick={boundOnDelete}><Clear/></IconButton></Tooltip>;
}
const forms = (this.props.form.items as JsonFormData[]).map((form, index) => {
const copy = this.copyWithIndex(form, i);
return this.props.builder(copy, this.props.model, index, this.props.onChange,
this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen,
this.props.onHelpClick, this.props.mapper);
});
arrays.push(
<li key={keys[i]} className='list-group-item'>
{removeButton}
{forms}
</li>
);
}
let addButton: JSX.Element = null;
if (!this.props.form.readonly) {
addButton = <Button variant='contained'
color='primary'
startIcon={<Add/>}
style={{marginBottom: '8px'}}
onClick={this.onAppend}>{this.props.form.add || 'New'}</Button>;
}
return (
<div>
<div className='tb-container'>
<div className='tb-head-label'>{this.props.form.title}</div>
<ol className='list-group'>
{arrays}
</ol>
</div>
{addButton}
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardArray);

122
ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx

@ -1,122 +0,0 @@
/*
* 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 * as React from 'react';
import JsonFormUtils from './json-form-utils';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import { isDefinedAndNotNull } from '@core/utils';
export default ThingsboardBaseComponent => class<P extends JsonFormFieldProps>
extends React.Component<P, JsonFormFieldState> {
constructor(props: P) {
super(props);
this.onChangeValidate = this.onChangeValidate.bind(this);
const value = this.defaultValue();
const validationResult = JsonFormUtils.validate(this.props.form, value);
this.state = {
value,
valid: !!(validationResult.valid || !value),
error: !validationResult.valid && value ? validationResult.error.message : null
};
}
componentDidMount() {
if (typeof this.state.value !== 'undefined') {
this.props.onChange(this.props.form.key, this.state.value);
}
}
onChangeValidate(e, forceUpdate?: boolean) {
let value = null;
if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') {
if (!e || e.target?.value === null || e.target?.value === '') {
value = undefined;
} else if (typeof e === 'number') {
value = Number(e);
} else if (e.target.value.indexOf('.') === -1) {
value = parseInt(e.target.value, 10);
} else {
value = parseFloat(e.target.value);
}
} else if (this.props.form.schema.type === 'boolean') {
value = e.target.checked;
} else if (this.props.form.schema.type === 'date' || this.props.form.schema.type === 'array') {
value = e;
} else { // string
value = e.target.value;
}
const validationResult = JsonFormUtils.validate(this.props.form, value);
this.setState({
value,
valid: validationResult.valid,
error: validationResult.valid ? null : validationResult.error.message
});
this.props.onChange(this.props.form.key, value, forceUpdate);
}
defaultValue() {
let value = JsonFormUtils.selectOrSet(this.props.form.key, this.props.model);
if (this.props.form.schema.type === 'boolean') {
if (typeof value !== 'boolean' && typeof this.props.form.default === 'boolean') {
value = this.props.form.default;
}
if (typeof value !== 'boolean' && this.props.form.schema && typeof this.props.form.schema.default === 'boolean') {
value = this.props.form.schema.default;
}
if (typeof value !== 'boolean' &&
this.props.form.schema &&
this.props.form.required) {
value = false;
}
} else if (this.props.form.schema.type === 'integer' || this.props.form.schema.type === 'number') {
if (typeof value !== 'number' && typeof this.props.form.default === 'number') {
value = this.props.form.default;
}
if (typeof value !== 'number' && this.props.form.schema && typeof this.props.form.schema.default === 'number') {
value = this.props.form.schema.default;
}
if (typeof value !== 'number' && this.props.form.titleMap && typeof this.props.form.titleMap[0].value === 'number') {
value = this.props.form.titleMap[0].value;
}
if (value && typeof value === 'string') {
if (value.indexOf('.') === -1) {
value = parseInt(value, 10);
} else {
value = parseFloat(value);
}
}
} else {
if (!value && isDefinedAndNotNull(this.props.form.default)) {
value = this.props.form.default;
}
if (!value && this.props.form.schema && isDefinedAndNotNull(this.props.form.schema.default)) {
value = this.props.form.schema.default;
}
if (!value && this.props.form.titleMap && isDefinedAndNotNull(this.props.form.titleMap[0].value)) {
value = this.props.form.titleMap[0].value;
}
}
return value;
}
render() {
if (this.props.form && this.props.form.schema) {
return <ThingsboardBaseComponent {...this.props} {...this.state} onChangeValidate={this.onChangeValidate}/>;
} else {
return <div></div>;
}
}
};

46
ui-ngx/src/app/shared/components/json-form/react/json-form-checkbox.tsx

@ -1,46 +0,0 @@
/*
* 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 * as React from 'react';
import ThingsboardBaseComponent from './json-form-base-component';
import Checkbox from '@mui/material/Checkbox';
import { JsonFormFieldProps, JsonFormFieldState } from './json-form.models.js';
import FormControlLabel from '@mui/material/FormControlLabel';
class ThingsboardCheckbox extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
render() {
return (
<div>
<FormControlLabel
control={
<Checkbox
color={'secondary'}
name={this.props.form.key.slice(-1)[0] + ''}
value={this.props.form.key.slice(-1)[0]}
checked={this.props.value || false}
disabled={this.props.form.readonly}
onChange={(e) => {
this.props.onChangeValidate(e);
}}
/>
}
label={this.props.form.title}
/>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardCheckbox);

186
ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx

@ -1,186 +0,0 @@
/*
* 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 * as React from 'react';
import ThingsboardBaseComponent from './json-form-base-component';
import reactCSS from 'reactcss';
import tinycolor from 'tinycolor2';
import TextField from '@mui/material/TextField';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import IconButton from '@mui/material/IconButton';
import Clear from '@mui/icons-material/Clear';
import Tooltip from '@mui/material/Tooltip';
interface ThingsboardColorState extends JsonFormFieldState {
color: tinycolor.ColorFormats.RGBA | null;
focused: boolean;
}
class ThingsboardColor extends React.Component<JsonFormFieldProps, ThingsboardColorState> {
containerRef = React.createRef<HTMLDivElement>();
constructor(props: JsonFormFieldProps) {
super(props);
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onValueChanged = this.onValueChanged.bind(this);
this.onSwatchClick = this.onSwatchClick.bind(this);
this.onClear = this.onClear.bind(this);
const value = props.value ? props.value + '' : null;
const color = value != null ? tinycolor(value).toRgb() : null;
this.state = {
color,
focused: false
};
}
onBlur() {
this.setState({focused: false});
}
onFocus() {
this.setState({focused: true});
}
componentDidMount() {
const node = this.containerRef.current;
const colContainer = $(node).children('#color-container');
colContainer.click(() => {
if (!this.props.form.readonly) {
this.onSwatchClick();
}
});
}
componentWillUnmount() {
const node = this.containerRef.current;
const colContainer = $(node).children('#color-container');
colContainer.off( 'click' );
}
onValueChanged(value: tinycolor.ColorFormats.RGBA | null) {
let color: tinycolor.Instance = null;
if (value != null) {
color = tinycolor(value);
}
this.setState({
color: value
});
let colorValue = '';
if (color != null && color.getAlpha() !== 1) {
colorValue = color.toRgbString();
} else if (color != null) {
colorValue = color.toHexString();
}
this.props.onChangeValidate({
target: {
value: colorValue
}
});
}
onSwatchClick() {
this.props.onColorClick(this.props.form.key, this.state.color,
(color) => {
this.onValueChanged(color);
}
);
}
onClear(event: React.MouseEvent) {
if (event) {
event.stopPropagation();
}
this.onValueChanged(null);
}
render() {
let background = 'rgba(0,0,0,0)';
if (this.state.color != null) {
background = `rgba(${ this.state.color.r }, ${ this.state.color.g }, ${ this.state.color.b }, ${ this.state.color.a })`;
}
const styles = reactCSS({
default: {
color: {
background: `${ background }`
},
swatch: {
display: 'inline-block',
marginRight: '10px',
marginTop: 'auto',
marginBottom: 'auto',
cursor: 'pointer',
opacity: `${ this.props.form.readonly ? '0.6' : '1' }`
},
swatchText: {
width: '100%'
},
container: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
colorContainer: {
display: 'flex',
width: '100%'
}
},
});
let fieldClass = 'tb-field';
if (this.props.form.required) {
fieldClass += ' tb-required';
}
if (this.props.form.readonly) {
fieldClass += ' tb-readonly';
}
if (this.state.focused) {
fieldClass += ' tb-focused';
}
let stringColor = '';
if (this.state.color != null) {
const color = tinycolor(this.state.color);
stringColor = color.toRgbString();
}
return (
<div ref={this.containerRef} style={ styles.container }>
<div id='color-container' style={ styles.colorContainer }>
<div className='tb-color-preview' style={ styles.swatch }>
<div className='tb-color-result' style={ styles.color }/>
</div>
<TextField
variant={'standard'}
className={fieldClass}
label={this.props.form.title}
error={!this.props.valid}
helperText={this.props.valid ? this.props.form.placeholder : this.props.error}
value={stringColor}
disabled={this.props.form.readonly}
onFocus={this.onFocus}
onBlur={this.onBlur}
style={ styles.swatchText }/>
</div>
<Tooltip title='Clear' placement='top'><IconButton onClick={this.onClear}><Clear/></IconButton></Tooltip>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardColor);

40
ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx

@ -1,40 +0,0 @@
/*
* 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 * as React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import { Observable } from 'rxjs/internal/Observable';
import { beautifyCss } from '@shared/models/beautify.models';
class ThingsboardCss extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
constructor(props: JsonFormFieldProps) {
super(props);
this.onTidyCss = this.onTidyCss.bind(this);
}
onTidyCss(css: string): Observable<string> {
return beautifyCss(css, {indent_size: 4});
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='css' onTidy={this.onTidyCss} {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardCss;

83
ui-ngx/src/app/shared/components/json-form/react/json-form-date.tsx

@ -1,83 +0,0 @@
/*
* 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 * as React from 'react';
import ThingsboardBaseComponent from './json-form-base-component';
import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'
import { LocalizationProvider, DatePicker } from '@mui/x-date-pickers';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import moment from 'moment';
interface ThingsboardDateState extends JsonFormFieldState {
currentValue: Date | null;
}
class ThingsboardDate extends React.Component<JsonFormFieldProps, ThingsboardDateState> {
constructor(props: JsonFormFieldProps) {
super(props);
this.onDatePicked = this.onDatePicked.bind(this);
let value: Date | null = null;
if (this.props.value && typeof this.props.value === 'number') {
value = new Date(this.props.value);
}
this.state = {
currentValue: value
};
}
onDatePicked(date: moment.Moment | null) {
this.setState({
currentValue: date?.toDate()
});
this.props.onChangeValidate(date ? date.valueOf() : null);
}
render() {
let fieldClass = 'tb-date-field';
if (this.props.form.required) {
fieldClass += ' tb-required';
}
if (this.props.form.readonly) {
fieldClass += ' tb-readonly';
}
return (
<LocalizationProvider dateAdapter={AdapterMoment}>
<div style={{width: '100%', display: 'block'}}>
<DatePicker
slotProps={{
textField: {
variant: 'standard'
}
}}
format='MM/DD/YYYY'
className={fieldClass}
label={this.props.form.title}
value={moment(this.state.currentValue?.valueOf())}
onChange={this.onDatePicked}
disabled={this.props.form.readonly}
sx={this.props.form.style || {width: '100%'}}
/>
</div>
</LocalizationProvider>
);
}
}
export default ThingsboardBaseComponent(ThingsboardDate);

44
ui-ngx/src/app/shared/components/json-form/react/json-form-fieldset.tsx

@ -1,44 +0,0 @@
/*
* 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 * as React from 'react';
import {
JsonFormData,
JsonFormFieldProps,
JsonFormFieldState
} from '@shared/components/json-form/react/json-form.models';
class ThingsboardFieldSet extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
render() {
const forms = (this.props.form.items as JsonFormData[]).map((form: JsonFormData, index) => {
return this.props.builder(form, this.props.model, index, this.props.onChange,
this.props.onColorClick, this.props.onIconClick, this.props.onToggleFullscreen, this.props.onHelpClick, this.props.mapper);
});
return (
<div style={{paddingTop: '20px'}}>
<div className='tb-head-label'>
{this.props.form.title}
</div>
<div>
{forms}
</div>
</div>
);
}
}
export default ThingsboardFieldSet;

40
ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx

@ -1,40 +0,0 @@
/*
* 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 * as React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import { Observable } from 'rxjs/internal/Observable';
import { beautifyHtml } from '@shared/models/beautify.models';
class ThingsboardHtml extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
constructor(props: JsonFormFieldProps) {
super(props);
this.onTidyHtml = this.onTidyHtml.bind(this);
}
onTidyHtml(html: string): Observable<string> {
return beautifyHtml(html, {indent_size: 4});
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='html' onTidy={this.onTidyHtml} {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardHtml;

162
ui-ngx/src/app/shared/components/json-form/react/json-form-icon.tsx

@ -1,162 +0,0 @@
/*
* 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 * as React from 'react';
import { MouseEvent } from 'react';
import ThingsboardBaseComponent from './json-form-base-component';
import reactCSS from 'reactcss';
import TextField from '@mui/material/TextField';
import IconButton from '@mui/material/IconButton';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import Clear from '@mui/icons-material/Clear';
import Icon from '@mui/material/Icon';
import Tooltip from '@mui/material/Tooltip';
interface ThingsboardIconState extends JsonFormFieldState {
icon: string | null;
focused: boolean;
}
class ThingsboardIcon extends React.Component<JsonFormFieldProps, ThingsboardIconState> {
containerRef = React.createRef<HTMLDivElement>();
constructor(props: JsonFormFieldProps) {
super(props);
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.onValueChanged = this.onValueChanged.bind(this);
this.onIconClick = this.onIconClick.bind(this);
this.onClear = this.onClear.bind(this);
const icon = props.value ? props.value : '';
this.state = {
icon,
focused: false
};
}
onBlur() {
this.setState({focused: false});
}
onFocus() {
this.setState({focused: true});
}
componentDidMount() {
const node = this.containerRef.current;
const iconContainer = $(node).children('#icon-container');
iconContainer.on('click', (event) => {
if (!this.props.form.readonly) {
this.onIconClick(event);
}
});
}
componentWillUnmount() {
const node = this.containerRef.current;
const iconContainer = $(node).children('#icon-container');
iconContainer.off( 'click' );
}
onValueChanged(value: string | null) {
this.setState({
icon: value
});
this.props.onChange(this.props.form.key, value);
}
onIconClick(_event) {
this.props.onIconClick(this.props.form.key, this.state.icon,
(color) => {
this.onValueChanged(color);
}
);
}
onClear(event: MouseEvent) {
if (event) {
event.stopPropagation();
}
this.onValueChanged('');
}
render() {
const styles = reactCSS({
default: {
container: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
icon: {
padding: '12px',
marginRight: '10px',
marginBottom: 'auto',
cursor: 'pointer',
border: 'solid 1px rgba(0, 0, 0, .27)',
borderRadius: '0'
},
iconContainer: {
display: 'flex',
width: '100%'
},
iconText: {
width: '100%'
},
},
});
let fieldClass = 'tb-field';
if (this.props.form.required) {
fieldClass += ' tb-required';
}
if (this.state.focused) {
fieldClass += ' tb-focused';
}
let pickedIcon = 'more_horiz';
let icon = '';
if (this.state.icon !== '') {
pickedIcon = this.state.icon;
icon = this.state.icon;
}
return (
<div ref={this.containerRef} style={ styles.container }>
<div id='icon-container' style={ styles.iconContainer }>
<IconButton style={ styles.icon }>
<Icon>{pickedIcon}</Icon>
</IconButton>
<TextField
variant={'standard'}
className={fieldClass}
label={this.props.form.title}
error={!this.props.valid}
helperText={this.props.valid ? this.props.form.placeholder : this.props.error}
value={icon}
disabled={this.props.form.readonly}
onFocus={this.onFocus}
onBlur={this.onBlur}
style={ styles.iconText } />
</div>
<Tooltip title='Clear' placement='top'><IconButton onClick={this.onClear}><Clear/></IconButton></Tooltip>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardIcon);

108
ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx

@ -1,108 +0,0 @@
/*
* 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 React, { MouseEvent } from 'react';
import Dropzone from 'react-dropzone';
import ThingsboardBaseComponent from './json-form-base-component';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import IconButton from '@mui/material/IconButton';
import Clear from '@mui/icons-material/Clear';
import Tooltip from '@mui/material/Tooltip';
interface ThingsboardImageState extends JsonFormFieldState {
imageUrl: string;
}
class ThingsboardImage extends React.Component<JsonFormFieldProps, ThingsboardImageState> {
constructor(props: JsonFormFieldProps) {
super(props);
this.onDrop = this.onDrop.bind(this);
this.onClear = this.onClear.bind(this);
const value = props.value ? props.value + '' : null;
this.state = {
imageUrl: value
};
}
onDrop(acceptedFiles: File[]) {
const reader = new FileReader();
reader.onload = () => {
this.onValueChanged(reader.result as string);
};
reader.readAsDataURL(acceptedFiles[0]);
}
onValueChanged(value: string) {
this.setState({
imageUrl: value
});
this.props.onChangeValidate({
target: {
value
}
});
}
onClear(event: MouseEvent) {
if (event) {
event.stopPropagation();
}
this.onValueChanged('');
}
render() {
let labelClass = 'tb-label';
if (this.props.form.required) {
labelClass += ' tb-required';
}
if (this.props.form.readonly) {
labelClass += ' tb-readonly';
}
let previewComponent: React.JSX.Element;
if (this.state.imageUrl) {
previewComponent = <img className='tb-image-preview' src={this.state.imageUrl} />;
} else {
previewComponent = <div>No image selected</div>;
}
return (
<div className='tb-container'>
<label className={labelClass}>{this.props.form.title}</label>
<div className='tb-image-select-container'>
<div className='tb-image-preview-container'>{previewComponent}</div>
<div className='tb-image-clear-container'>
<Tooltip title='Clear' placement='top'>
<IconButton className='tb-image-clear-btn' onClick={this.onClear}><Clear/></IconButton>
</Tooltip>
</div>
<Dropzone onDrop={this.onDrop}
accept={ {'image/*': []} } multiple={false}>
{({getRootProps, getInputProps}) => (
<div className='tb-dropzone' {...getRootProps()}>
<div>Drop an image or click to select a file to upload.</div>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
</div>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardImage);

40
ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx

@ -1,40 +0,0 @@
/*
* 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 * as React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import { Observable } from 'rxjs/internal/Observable';
import { beautifyJs } from '@shared/models/beautify.models';
class ThingsboardJavaScript extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
constructor(props: JsonFormFieldProps) {
super(props);
this.onTidyJavascript = this.onTidyJavascript.bind(this);
}
onTidyJavascript(javascript: string): Observable<string> {
return beautifyJs(javascript, {indent_size: 4, wrap_line_length: 60});
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='javascript' onTidy={this.onTidyJavascript} {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardJavaScript;

40
ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx

@ -1,40 +0,0 @@
/*
* 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 * as React from 'react';
import ThingsboardAceEditor from './json-form-ace-editor';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import { Observable } from 'rxjs/internal/Observable';
import { beautifyJs } from '@shared/models/beautify.models';
class ThingsboardJson extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
constructor(props: JsonFormFieldProps) {
super(props);
this.onTidyJson = this.onTidyJson.bind(this);
}
onTidyJson(json: string): Observable<string> {
return beautifyJs(json, {indent_size: 4});
}
render() {
return (
<ThingsboardAceEditor {...this.props} mode='json' onTidy={this.onTidyJson} {...this.state}></ThingsboardAceEditor>
);
}
}
export default ThingsboardJson;

99
ui-ngx/src/app/shared/components/json-form/react/json-form-number.tsx

@ -1,99 +0,0 @@
/*
* 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 * as React from 'react';
import ThingsboardBaseComponent from './json-form-base-component';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import { TextField } from '@mui/material';
import { ChangeEvent } from 'react';
interface ThingsboardNumberState extends JsonFormFieldState {
focused: boolean;
lastSuccessfulValue: number;
}
class ThingsboardNumber extends React.Component<JsonFormFieldProps, ThingsboardNumberState> {
constructor(props: JsonFormFieldProps) {
super(props);
this.preValidationCheck = this.preValidationCheck.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.state = {
lastSuccessfulValue: this.props.value,
focused: false
};
}
isNumeric(n: any) {
return n === null || n === '' || !isNaN(n) && isFinite(n);
}
onBlur() {
this.setState({focused: false});
}
onFocus() {
this.setState({focused: true});
}
preValidationCheck(e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) {
if (this.isNumeric(e.target.value)) {
this.setState({
lastSuccessfulValue: Number(e.target.value)
});
this.props.onChangeValidate(e);
}
}
render() {
let fieldClass = 'tb-field';
if (this.props.form.required) {
fieldClass += ' tb-required';
}
if (this.props.form.readonly) {
fieldClass += ' tb-readonly';
}
if (this.state.focused) {
fieldClass += ' tb-focused';
}
let value = this.state.lastSuccessfulValue;
if (typeof value !== 'undefined') {
value = Number(value);
} else {
value = null;
}
return (
<div>
<TextField
variant={'standard'}
className={fieldClass}
label={this.props.form.title}
type='number'
error={!this.props.valid}
helperText={this.props.valid ? this.props.form.placeholder : this.props.error}
onChange={this.preValidationCheck}
defaultValue={value}
disabled={this.props.form.readonly}
onFocus={this.onFocus}
onBlur={this.onBlur}
style={this.props.form.style || {width: '100%'}}/>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardNumber);

51
ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx

@ -1,51 +0,0 @@
/*
* 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 * as React from 'react';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import FormControlLabel from '@mui/material/FormControlLabel';
import { FormLabel, Radio, RadioGroup } from '@mui/material';
import FormControl from '@mui/material/FormControl';
import ThingsboardBaseComponent from '@shared/components/json-form/react/json-form-base-component';
class ThingsboardRadios extends React.Component<JsonFormFieldProps, JsonFormFieldState> {
render() {
const items = this.props.form.titleMap.map((item, index) => {
return (
<FormControlLabel value={item.value} control={<Radio color={'secondary'} />} label={item.name} key={index} />
);
});
let row = false;
if (this.props.form.direction === 'row') {
row = true;
}
return (
<FormControl component='fieldset'
className={this.props.form.htmlClass}
disabled={this.props.form.readonly}>
<FormLabel component='legend'>{this.props.form.title}</FormLabel>
<RadioGroup row={row} name={this.props.form.title} value={this.props.value} onChange={(e) => {
this.props.onChangeValidate(e);
}}>
{items}
</RadioGroup>
</FormControl>
);
}
}
export default ThingsboardBaseComponent(ThingsboardRadios);

202
ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx

@ -1,202 +0,0 @@
/*
* 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 * as React from 'react';
import ThingsboardBaseComponent from './json-form-base-component';
import Select, { Option } from 'rc-select';
import {
JsonFormFieldProps,
JsonFormFieldState,
KeyLabelItem
} from '@shared/components/json-form/react/json-form.models';
import { Mode } from 'rc-select/lib/interface';
import { deepClone } from '@core/utils';
interface ThingsboardRcSelectState extends JsonFormFieldState {
currentValue: KeyLabelItem | KeyLabelItem[];
items: Array<KeyLabelItem>;
focused: boolean;
}
class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, ThingsboardRcSelectState> {
constructor(props: JsonFormFieldProps) {
super(props);
this.onSelect = this.onSelect.bind(this);
this.onDeselect = this.onDeselect.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.state = {
currentValue: this.keyToCurrentValue(this.props.value, this.props.form.schema.type === 'array'),
items: this.props.form.items as KeyLabelItem[],
focused: false
};
}
keyToCurrentValue(key: string | string[], isArray: boolean): KeyLabelItem | KeyLabelItem[] {
let currentValue: KeyLabelItem | KeyLabelItem[] = isArray ? [] : null;
if (isArray) {
const keys = key;
if (keys) {
(keys as string[]).forEach((keyVal) => {
(currentValue as KeyLabelItem[]).push({key: keyVal, label: this.labelFromKey(keyVal)});
});
}
} else {
currentValue = {key: key as string, label: this.labelFromKey(key as string)};
}
return currentValue;
}
labelFromKey(key: string): string {
let label = key || '';
if (key) {
for (const item of this.props.form.items) {
if (item.value === key) {
label = item.label;
break;
}
}
}
return label;
}
arrayValues(items: KeyLabelItem[]): string[] {
const v: string[] = [];
if (items) {
items.forEach(item => {
v.push(item.key);
});
}
return v;
}
keyIndex(values: KeyLabelItem[], key: string): number {
let index = -1;
if (values) {
for (let i = 0; i < values.length; i++) {
if (values[i].key === key) {
index = i;
break;
}
}
}
return index;
}
onSelect(value: KeyLabelItem) {
if (this.props.form.schema.type === 'array') {
const v = this.state.currentValue as KeyLabelItem[];
v.push(this.keyToCurrentValue(value.key, false) as KeyLabelItem);
this.setState({
currentValue: v
});
this.props.onChangeValidate(this.arrayValues(v));
} else {
this.setState({currentValue: this.keyToCurrentValue(value.key, false)});
this.props.onChangeValidate({target: {value: value.key}});
}
}
onDeselect(value: KeyLabelItem) {
if (this.props.form.schema.type === 'array') {
const v = this.state.currentValue as KeyLabelItem[];
const index = this.keyIndex(v, value.key);
if (index > -1) {
v.splice(index, 1);
}
this.setState({
currentValue: v
});
this.props.onChangeValidate(this.arrayValues(v));
}
}
onBlur() {
this.setState({ focused: false });
}
onFocus() {
this.setState({ focused: true });
}
render() {
let options: React.JSX.Element[] = [];
if (this.state.items && this.state.items.length > 0) {
options = this.state.items.map((item) => (
<Option key={item.value} value={item.value}>{item.label}</Option>
));
}
let labelClass = 'tb-label';
if (this.props.form.required) {
labelClass += ' tb-required';
}
if (this.props.form.readonly) {
labelClass += ' tb-readonly';
}
if (this.state.focused) {
labelClass += ' tb-focused';
}
let mode: Mode;
let value = this.state.currentValue;
if (this.props.form.tags || this.props.form.multiple) {
value = deepClone(value);
if (this.props.form.tags) {
mode = 'tags';
} else if (this.props.form.multiple) {
mode = 'multiple';
}
}
const dropdownStyle = {...this.props.form.dropdownStyle, ...{zIndex: 100001}};
let dropdownClassName = 'tb-rc-select-dropdown';
if (this.props.form.dropdownClassName) {
dropdownClassName += ' ' + this.props.form.dropdownClassName;
}
return (
<div className='tb-container'>
<label className={labelClass}>{this.props.form.title}</label>
<Select
className={this.props.form.className}
dropdownClassName={dropdownClassName}
dropdownStyle={dropdownStyle}
allowClear={this.props.form.allowClear}
showSearch={true}
mode={mode}
maxTagTextLength={this.props.form.maxTagTextLength}
disabled={this.props.form.readonly}
optionLabelProp='children'
value={value}
labelInValue={true}
onSelect={this.onSelect}
onDeselect={this.onDeselect}
onFocus={this.onFocus}
onBlur={this.onBlur}
placeholder={this.props.form.placeholder}
style={this.props.form.style || {width: '100%'}}>
{options}
</Select>
<div className='json-form-error'
style={{opacity: this.props.valid ? '0' : '1',
bottom: '-5px'}}>{this.props.error}</div>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardRcSelect);

53
ui-ngx/src/app/shared/components/json-form/react/json-form-react.tsx

@ -1,53 +0,0 @@
/*
* 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 * as React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import thingsboardTheme from './styles/thingsboardTheme';
import ThingsboardSchemaForm from './json-form-schema-form';
import { JsonFormProps } from './json-form.models';
const tbTheme = createTheme(thingsboardTheme);
class ReactSchemaForm extends React.Component<JsonFormProps, {}> {
static defaultProps: JsonFormProps;
constructor(props) {
super(props);
}
render() {
if (this.props.form.length > 0) {
return <ThemeProvider theme={tbTheme}><ThingsboardSchemaForm {...this.props} /></ThemeProvider>;
} else {
return <div></div>;
}
}
}
ReactSchemaForm.defaultProps = {
isFullscreen: false,
schema: {},
form: ['*'],
groupInfoes: [],
option: {
formDefaults: {
startEmpty: true
}
}
};
export default ReactSchemaForm;

217
ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx

@ -1,217 +0,0 @@
/*
* 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 * as React from 'react';
import JsonFormUtils from './json-form-utils';
import ThingsboardArray from './json-form-array';
import ThingsboardJavaScript from './json-form-javascript';
import ThingsboardJson from './json-form-json';
import ThingsboardHtml from './json-form-html';
import ThingsboardCss from './json-form-css';
import ThingsboardColor from './json-form-color';
import ThingsboardRcSelect from './json-form-rc-select';
import ThingsboardNumber from './json-form-number';
import ThingsboardText from './json-form-text';
import ThingsboardSelect from './json-form-select';
import ThingsboardRadios from './json-form-radios';
import ThingsboardDate from './json-form-date';
import ThingsboardImage from './json-form-image';
import ThingsboardCheckbox from './json-form-checkbox';
import ThingsboardHelp from './json-form-help';
import ThingsboardFieldSet from './json-form-fieldset';
import ThingsboardIcon from './json-form-icon';
import {
JsonFormData,
JsonFormProps,
onChangeFn,
OnColorClickFn, onHelpClickFn,
OnIconClickFn,
onToggleFullscreenFn
} from './json-form.models';
import _ from 'lodash';
import tinycolor from 'tinycolor2';
import { GroupInfo } from '@shared/models/widget.models';
import ThingsboardMarkdown from '@shared/components/json-form/react/json-form-markdown';
import { MouseEvent, ReactNode } from 'react';
class ThingsboardSchemaForm extends React.Component<JsonFormProps, any> {
private hasConditions: boolean;
private conditionFunction: Function;
private readonly mapper: {[type: string]: any};
constructor(props: JsonFormProps) {
super(props);
this.mapper = {
number: ThingsboardNumber,
text: ThingsboardText,
password: ThingsboardText,
textarea: ThingsboardText,
select: ThingsboardSelect,
radios: ThingsboardRadios,
date: ThingsboardDate,
image: ThingsboardImage,
checkbox: ThingsboardCheckbox,
help: ThingsboardHelp,
array: ThingsboardArray,
javascript: ThingsboardJavaScript,
json: ThingsboardJson,
html: ThingsboardHtml,
css: ThingsboardCss,
markdown: ThingsboardMarkdown,
color: ThingsboardColor,
'rc-select': ThingsboardRcSelect,
fieldset: ThingsboardFieldSet,
icon: ThingsboardIcon
};
this.onChange = this.onChange.bind(this);
this.onColorClick = this.onColorClick.bind(this);
this.onIconClick = this.onIconClick.bind(this);
this.onToggleFullscreen = this.onToggleFullscreen.bind(this);
this.onHelpClick = this.onHelpClick.bind(this);
this.hasConditions = false;
}
onChange(key: (string | number)[], val: any, forceUpdate?: boolean) {
this.props.onModelChange(key, val, forceUpdate);
if (this.hasConditions) {
this.forceUpdate();
}
}
onColorClick(key: (string | number)[], val: tinycolor.ColorFormats.RGBA,
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) {
this.props.onColorClick(key, val, colorSelectedFn);
}
onIconClick(key: (string | number)[], val: string,
iconSelectedFn: (icon: string) => void) {
this.props.onIconClick(key, val, iconSelectedFn);
}
onToggleFullscreen(fullscreenFinishFn?: (el: Element) => void) {
this.props.onToggleFullscreen(fullscreenFinishFn);
}
onHelpClick(event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void, helpReadyFn: (ready: boolean) => void) {
this.props.onHelpClick(event, helpId, helpVisibleFn, helpReadyFn);
}
builder(form: JsonFormData,
model: any,
index: number,
onChange: onChangeFn,
onColorClick: OnColorClickFn,
onIconClick: OnIconClickFn,
onToggleFullscreen: onToggleFullscreenFn,
onHelpClick: onHelpClickFn,
mapper: {[type: string]: any}): React.JSX.Element {
const type = form.type;
const Field = this.mapper[type];
if (!Field) {
console.log('Invalid field: \"' + form.key[0] + '\"!');
return null;
}
if (form.condition) {
this.hasConditions = true;
if (!form.conditionFunction) {
form.conditionFunction = new Function('form', 'model', 'index', `return ${form.condition};`);
}
if (form.conditionFunction(form, model, index) === false) {
return null;
}
}
return <Field model={model} form={form} key={index} onChange={onChange}
onColorClick={onColorClick}
onIconClick={onIconClick}
onToggleFullscreen={onToggleFullscreen}
onHelpClick={onHelpClick}
mapper={mapper} builder={this.builder}/>;
}
createSchema(theForm: any[]): React.JSX.Element {
const merged = JsonFormUtils.merge(this.props.schema, theForm, this.props.ignore, this.props.option);
let mapper = this.mapper;
if (this.props.mapper) {
mapper = _.merge(this.mapper, this.props.mapper);
}
const forms: ReactNode[] = merged.map(function(form: JsonFormData, index: number) {
return this.builder(form, this.props.model, index, this.onChange, this.onColorClick,
this.onIconClick, this.onToggleFullscreen, this.onHelpClick, mapper);
}.bind(this));
let formClass = 'SchemaForm';
if (this.props.isFullscreen) {
formClass += ' SchemaFormFullscreen';
}
return (
<div style={{width: '100%'}} className={formClass}>{forms}</div>
);
}
render() {
if (this.props.groupInfoes && this.props.groupInfoes.length > 0) {
const content: React.JSX.Element[] = [];
for (const info of this.props.groupInfoes) {
const forms = this.createSchema(this.props.form[info.formIndex]);
const item = <ThingsboardSchemaGroup key={content.length} forms={forms} info={info}></ThingsboardSchemaGroup>;
content.push(item);
}
return (<div>{content}</div>);
} else {
return this.createSchema(this.props.form);
}
}
}
export default ThingsboardSchemaForm;
interface ThingsboardSchemaGroupProps {
info: GroupInfo;
forms: React.JSX.Element;
}
interface ThingsboardSchemaGroupState {
showGroup: boolean;
}
class ThingsboardSchemaGroup extends React.Component<ThingsboardSchemaGroupProps, ThingsboardSchemaGroupState> {
constructor(props: ThingsboardSchemaGroupProps) {
super(props);
this.state = {
showGroup: true
};
}
toogleGroup() {
this.setState({
showGroup: !this.state.showGroup
});
}
render() {
const theCla = 'pull-right fa fa-chevron-down tb-toggle-icon' + (this.state.showGroup ? '' : ' tb-toggled');
return (<section className='mat-elevation-z1' style={{marginTop: '10px'}}>
<div className='SchemaGroupname tb-button-toggle'
onClick={this.toogleGroup.bind(this)}>{this.props.info.GroupTitle}<span className={theCla}></span></div>
<div style={{padding: '20px'}} className={this.state.showGroup ? '' : 'invisible'}>{this.props.forms}</div>
</section>);
}
}

93
ui-ngx/src/app/shared/components/json-form/react/json-form-select.tsx

@ -1,93 +0,0 @@
/*
* 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 * as React from 'react';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import ThingsboardBaseComponent from '@shared/components/json-form/react/json-form-base-component';
import { isObject } from '@core/utils';
interface ThingsboardSelectState extends JsonFormFieldState {
currentValue: any;
}
class ThingsboardSelect extends React.Component<JsonFormFieldProps, ThingsboardSelectState> {
static getDerivedStateFromProps(props: JsonFormFieldProps) {
if (props.model && props.form.key) {
return {
currentValue: ThingsboardSelect.getModelKey(props.model, props.form.key)
|| (props.form.titleMap != null ? props.form.titleMap[0].value : '')
}
}
}
static getModelKey(model: any, key: (string | number)[]) {
if (Array.isArray(key)) {
const res = key.reduce((cur, nxt) => (cur[nxt] || {}), model);
if (res && isObject(res)) {
return undefined;
} else {
return res;
}
} else {
return model[key];
}
}
constructor(props: JsonFormFieldProps) {
super(props);
this.onSelected = this.onSelected.bind(this);
const possibleValue = ThingsboardSelect.getModelKey(this.props.model, this.props.form.key);
this.state = {
currentValue: this.props.model !== undefined && possibleValue ? possibleValue : this.props.form.titleMap != null ?
this.props.form.titleMap[0].value : ''
};
}
onSelected(event: SelectChangeEvent<any>) {
this.setState({
currentValue: event.target.value
});
this.props.onChangeValidate(event);
}
render() {
const menuItems = this.props.form.titleMap.map((item, idx) => (
<MenuItem key={idx}
value={item.value}>{item.name}</MenuItem>
));
return (
<FormControl className={this.props.form.htmlClass}
disabled={this.props.form.readonly}
fullWidth={true}>
<InputLabel variant={'standard'} htmlFor='select-field'>{this.props.form.title}</InputLabel>
<Select
variant={'standard'}
value={this.state.currentValue}
onChange={this.onSelected}>
{menuItems}
</Select>
</FormControl>
);
}
}
export default ThingsboardBaseComponent(ThingsboardSelect);

92
ui-ngx/src/app/shared/components/json-form/react/json-form-text.tsx

@ -1,92 +0,0 @@
/*
* 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 * as React from 'react';
import ThingsboardBaseComponent from './json-form-base-component';
import TextField from '@mui/material/TextField';
import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models';
interface ThingsboardTextState extends JsonFormFieldState {
focused: boolean;
}
class ThingsboardText extends React.Component<JsonFormFieldProps, ThingsboardTextState> {
constructor(props: JsonFormFieldProps) {
super(props);
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.state = {
focused: false
};
}
onBlur() {
this.setState({focused: false});
}
onFocus() {
this.setState({focused: true});
}
render() {
let fieldClass = 'tb-field';
if (this.props.form.required) {
fieldClass += ' tb-required';
}
if (this.props.form.readonly) {
fieldClass += ' tb-readonly';
}
if (this.state.focused) {
fieldClass += ' tb-focused';
}
const multiline = this.props.form.type === 'textarea';
let rows = 1;
let rowsMax = 1;
let minHeight = 48;
if (multiline) {
rows = this.props.form.rows || 2;
rowsMax = this.props.form.rowsMax;
minHeight = 19 * rows + 48;
}
return (
<div>
<TextField
variant={'standard'}
className={fieldClass}
type={this.props.form.type}
label={this.props.form.title}
multiline={multiline}
error={!this.props.valid}
helperText={this.props.valid ? this.props.form.placeholder : this.props.error}
onChange={(e) => {
this.props.onChangeValidate(e);
}}
defaultValue={this.props.value}
disabled={this.props.form.readonly}
rows={rows}
maxRows={rowsMax}
onFocus={this.onFocus}
onBlur={this.onBlur}
style={this.props.form.style || {width: '100%', minHeight: minHeight + 'px'}}/>
</div>
);
}
}
export default ThingsboardBaseComponent(ThingsboardText);

141
ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts

@ -1,141 +0,0 @@
///
/// 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 tinycolor from 'tinycolor2';
import { GroupInfo } from '@shared/models/widget.models';
import { MouseEvent } from 'react';
export interface SchemaValidationResult {
valid: boolean;
error?: {
message?: string;
};
}
export interface FormOption {
formDefaults?: {
startEmpty?: boolean;
readonly?: boolean;
};
supressPropertyTitles?: boolean;
}
export interface DefaultsFormOptions {
global?: FormOption;
required?: boolean;
path?: string[];
lookup?: {[key: string]: any};
ignore?: {[key: string]: boolean};
}
export type onChangeFn = (key: (string | number)[], val: any, forceUpdate?: boolean) => void;
export type OnColorClickFn = (key: (string | number)[], val: tinycolor.ColorFormats.RGBA,
colorSelectedFn: (color: tinycolor.ColorFormats.RGBA) => void) => void;
export type OnIconClickFn = (key: (string | number)[], val: string,
iconSelectedFn: (icon: string) => void) => void;
export type onToggleFullscreenFn = (fullscreenFinishFn?: (el: Element) => void) => void;
export type onHelpClickFn = (event: MouseEvent, helpId: string, helpVisibleFn: (visible: boolean) => void,
helpReadyFn: (ready: boolean) => void) => void;
export interface JsonFormProps {
model?: any;
schema?: any;
form?: any;
groupInfoes?: GroupInfo[];
isFullscreen: boolean;
ignore?: {[key: string]: boolean};
option: FormOption;
onModelChange?: onChangeFn;
onColorClick?: OnColorClickFn;
onIconClick?: OnIconClickFn;
onToggleFullscreen?: onToggleFullscreenFn;
onHelpClick?: onHelpClickFn;
mapper?: {[type: string]: any};
}
export interface KeyLabelItem {
key: string;
label: string;
value?: string;
}
export interface JsonSchemaData {
type: string;
default: any;
items?: JsonSchemaData;
properties?: any;
}
export interface JsonFormData {
type: string;
key: (string | number)[];
title: string;
readonly: boolean;
required: boolean;
default?: any;
condition?: string;
conditionFunction?: Function;
style?: any;
rows?: number;
rowsMax?: number;
placeholder?: string;
schema: JsonSchemaData;
titleMap: {
value: any;
name: string;
}[];
items?: Array<KeyLabelItem> | Array<JsonFormData>;
tabs?: Array<JsonFormData>;
tags?: any;
helpId?: string;
startEmpty?: boolean;
[key: string]: any;
}
export type ComponentBuilderFn = (form: JsonFormData,
model: any,
index: number,
onChange: onChangeFn,
onColorClick: OnColorClickFn,
onIconClick: OnIconClickFn,
onToggleFullscreen: onToggleFullscreenFn,
onHelpClick: onHelpClickFn,
mapper: {[type: string]: any}) => JSX.Element;
export interface JsonFormFieldProps {
value: any;
model: any;
form: JsonFormData;
builder: ComponentBuilderFn;
mapper?: {[type: string]: any};
onChange?: onChangeFn;
onColorClick?: OnColorClickFn;
onIconClick?: OnIconClickFn;
onChangeValidate?: (e: any, forceUpdate?: boolean) => void;
onToggleFullscreen?: onToggleFullscreenFn;
onHelpClick?: onHelpClickFn;
valid?: boolean;
error?: string;
options?: {
setSchemaDefaults?: boolean;
};
}
export interface JsonFormFieldState {
value?: any;
valid?: boolean;
error?: string;
}

361
ui-ngx/src/app/shared/components/json-form/react/json-form.scss

@ -1,361 +0,0 @@
/**
* 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.
*/
$swift-ease-out-duration: .4s !default;
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
$input-label-float-offset: 6px !default;
$input-label-float-scale: .75 !default;
$previewSize: 100px !default;
.tb-json-form {
&.tb-fullscreen {
background: #fff;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
> div.fullscreen-form-field {
position: relative;
width: 100%;
height: 100%;
}
}
.json-form-error {
position: relative;
bottom: -5px;
font-size: 12px;
line-height: 12px;
color: rgb(244, 67, 54);
transition: all 450ms cubic-bezier(.23, 1, .32, 1) 0ms;
}
.tb-container {
position: relative;
box-sizing: border-box;
padding: 10px 0;
margin-top: 32px;
}
.tb-field {
padding-bottom: 18px;
.MuiInputBase-multiline {
flex: 1;
flex-direction: column;
.MuiInputBase-inputMultiline {
flex: 1;
}
}
&.tb-required {
label::after {
font-size: 13px;
color: rgba(0, 0, 0, .54);
vertical-align: top;
content: " *";
}
}
&.tb-focused:not(.tb-readonly) {
label::after {
color: rgb(221, 44, 0);
}
}
}
.tb-date-field {
&.tb-required {
div > div:first-child::after {
font-size: 13px;
color: rgba(0, 0, 0, .54);
vertical-align: top;
content: " *";
}
}
&.tb-focused:not(.tb-readonly) {
div > div:first-child::after {
color: rgb(221, 44, 0);
}
}
}
label.tb-label {
position: absolute;
right: auto;
bottom: 100%;
left: 0;
color: rgba(0, 0, 0, .54);
transition: transform $swift-ease-out-timing-function $swift-ease-out-duration, width $swift-ease-out-timing-function $swift-ease-out-duration;
transform: translate3d(0, $input-label-float-offset, 0) scale($input-label-float-scale);
transform-origin: left top;
-webkit-font-smoothing: antialiased;
&.tb-focused {
color: rgb(96, 125, 139);
}
&.tb-required::after {
font-size: 13px;
color: rgba(0, 0, 0, .54);
vertical-align: top;
content: " *";
}
&.tb-focused:not(.tb-readonly)::after {
color: rgb(221, 44, 0);
}
}
.tb-head-label {
color: rgba(0, 0, 0, .54);
padding-bottom: 15px;
}
.SchemaGroupname {
padding: 10px 20px;
background-color: #f1f1f1;
}
.invisible {
display: none;
}
.tb-button-toggle .tb-toggle-icon {
display: inline-block;
width: 15px;
margin: auto 0 auto auto;
background-size: 100% auto;
transition: transform .3s, ease-in-out;
}
.tb-button-toggle .tb-toggle-icon.tb-toggled {
transform: rotateZ(180deg);
}
.fullscreen-form-field {
.json-form-ace-editor {
height: calc(100% - 60px);
}
}
.json-form-ace-editor {
position: relative;
height: 100%;
border: 1px solid #c0c0c0;
.title-panel {
position: absolute;
top: 10px;
right: 20px;
z-index: 5;
font-size: .8rem;
font-weight: 500;
label {
padding: 4px;
color: #00acc1;
background: rgba(220, 220, 220, .35);
border-radius: 5px;
}
button.tidy-button {
background: rgba(220, 220, 220, .35) !important;
span {
padding: 0 !important;
font-size: 12px !important;
}
}
button.help-button {
background: rgba(220, 220, 220, .35);
padding: 4px;
}
div.help-button-loading {
pointer-events: none;
background: #f3f3f3;
border-radius: 50%;
display: flex;
place-content: center;
align-items: center;
}
}
}
.tb-image-select-container {
position: relative;
width: 100%;
height: $previewSize;
}
.tb-image-preview {
width: auto;
max-width: $previewSize;
height: auto;
max-height: $previewSize;
}
.tb-image-preview-container {
position: relative;
float: left;
width: $previewSize;
height: $previewSize;
margin-right: 12px;
vertical-align: top;
border: solid 1px;
div {
width: 100%;
font-size: 18px;
text-align: center;
}
div, .tb-image-preview {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.tb-dropzone {
outline: none;
position: relative;
height: $previewSize;
padding: 0 8px;
overflow: hidden;
vertical-align: top;
border: dashed 2px;
div {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
font-size: 24px;
text-align: center;
transform: translate(-50%, -50%);
}
}
.tb-image-clear-container {
position: relative;
float: right;
width: 48px;
height: $previewSize;
}
.tb-image-clear-btn {
position: absolute !important;
top: 50%;
transform: translate(0%, -50%) !important;
}
.MuiButton-root {
text-transform: none;
}
}
.rc-select {
box-sizing: border-box;
display: inline-block;
position: relative;
vertical-align: middle;
color: #666;
line-height: 28px;
font-size: inherit !important;
.rc-select-selector {
outline: none;
user-select: none;
box-sizing: border-box;
display: block;
background-color: #fff;
border-radius: 6px;
}
&.rc-select-single {
&:not(.rc-select-customize-input) {
.rc-select-selector {
height: 28px;
line-height: 28px;
position: relative;
border: 1px solid #d9d9d9;
&:hover {
border-color: #23c0fa;
box-shadow: 0 0 2px rgba(45, 183, 245, 0.8);
}
.rc-select-selection-search {
.rc-select-selection-search-input {
cursor: pointer;
background: transparent;
margin-left: 10px;
}
}
.rc-select-selection-item, .rc-select-selection-placeholder {
top: 0;
left: 10px;
}
}
&.rc-select-focused {
.rc-select-selector {
border-color: #23c0fa !important;
box-shadow: 0 0 2px rgba(45, 183, 245, 0.8) !important;
}
}
}
}
}
.rc-select-dropdown {
&.tb-rc-select-dropdown {
z-index: 100001;
background-color: white;
border: 1px solid #d9d9d9;
box-shadow: 0 0 4px #d9d9d9;
border-radius: 4px;
box-sizing: border-box;
outline: none;
.rc-select-item {
&.rc-select-item-option {
margin: 0;
position: relative;
display: block;
padding: 7px 10px;
font-weight: normal;
color: #666;
white-space: nowrap;
&.rc-select-item-option-selected {
color: #666;
background-color: #ddd;
}
&.rc-select-item-option-active {
background-color: #5897fb;
color: white;
cursor: pointer;
}
}
}
}
}

46
ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts

@ -1,46 +0,0 @@
///
/// 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 { indigo, deepOrange } from '@mui/material/colors';
import { ThemeOptions } from '@mui/material/styles';
import { PaletteOptions } from '@mui/material/styles/createPalette';
import { mergeDeep } from '@core/utils';
const PRIMARY_COLOR = '#305680';
const SECONDARY_COLOR = '#527dad';
const HUE3_COLOR = '#a7c1de';
const tbIndigo = mergeDeep<any>({}, indigo, {
500: PRIMARY_COLOR,
600: SECONDARY_COLOR,
700: PRIMARY_COLOR,
A100: HUE3_COLOR
});
const thingsboardPalette: PaletteOptions = {
primary: tbIndigo,
secondary: deepOrange,
background: {
default: '#eee'
}
};
export default {
typography: {
fontFamily: 'Roboto, \'Helvetica Neue\', sans-serif'
},
palette: thingsboardPalette,
} as ThemeOptions;

3
ui-ngx/src/app/shared/components/string-items-list.component.html

@ -15,7 +15,8 @@
limitations under the License.
-->
<mat-form-field [formGroup]="stringItemsForm" class="mat-block flex-1"
<mat-form-field [class]="fieldClass"
[formGroup]="stringItemsForm" class="mat-block flex-1"
[floatLabel]="floatLabel"
[appearance]="appearance"
[subscriptSizing]="subscriptSizing">

3
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[];

46
ui-ngx/src/app/shared/components/time/datetime.component.html

@ -15,26 +15,26 @@
limitations under the License.
-->
<section class="flex flex-row items-start justify-start gap-4">
<mat-form-field>
<mat-label *ngIf="showLabel">{{ dateText | translate }}</mat-label>
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #datePicker type="date" openOnFocus="true"></mat-datetimepicker>
<input [min]="minDateValue" [max]="maxDateValue"
[disabled]="disabled"
[required]="required"
matInput [(ngModel)]="date"
[matDatetimepicker]="datePicker" (ngModelChange)="onDateChange()">
</mat-form-field>
<mat-form-field>
<mat-label *ngIf="showLabel">{{ timeText | translate }}</mat-label>
<mat-datetimepicker-toggle [for]="timePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #timePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input [min]="minDateValue" [max]="maxDateValue"
[disabled]="disabled"
[required]="required"
matInput
[(ngModel)]="date"
[matDatetimepicker]="timePicker" (ngModelChange)="onDateChange()">
</mat-form-field>
</section>
<mat-form-field [class]="fieldClass"
[class.tb-datetime-field]="!fieldClass && type === 'datetime'"
[class.tb-date-field]="!fieldClass && type === 'date'"
[class.tb-time-field]="!fieldClass && type === 'time'"
[class.with-clear]="allowClear"
[subscriptSizing]="subscriptSizing" [appearance]="appearance" [floatLabel]="floatLabel">
<mat-label *ngIf="showLabel">{{ dateText }}</mat-label>
<div matSuffix class="mat-mdc-form-field-icon-suffix flex flex-row">
<button *ngIf="allowClear && date && !disabled"
type="button"
mat-icon-button aria-label="Clear"
(click)="clear($event)">
<mat-icon>close</mat-icon>
</button>
<mat-datetimepicker-toggle [for]="datePicker"></mat-datetimepicker-toggle>
</div>
<mat-datetimepicker #datePicker [type]="type" openOnFocus="true"></mat-datetimepicker>
<input [min]="minDateValue" [max]="maxDateValue"
[disabled]="disabled"
[required]="required"
matInput [(ngModel)]="date"
[matDatetimepicker]="datePicker" (ngModelChange)="onDateChange()">
</mat-form-field>

60
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);
}
}

32
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);
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save