diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index f1efd8943a..f605bfee8a 100644 --- a/ui-ngx/angular.json +++ b/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", diff --git a/ui-ngx/package.json b/ui-ngx/package.json index d8870a3568..9845d02637 100644 --- a/ui-ngx/package.json +++ b/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", diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 0d3718ee12..ebf390866c 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/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; diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 79fde27014..90c2547e09 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/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')) { diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 0cc3f5bb2f..257e1bf348 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts index 709159aec3..fe96529a1d 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts +++ b/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 diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html index 2b4ed91214..bb299bc4bc 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html @@ -101,8 +101,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html index c5b19aac1f..bc05db8c46 100644 --- a/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html @@ -47,8 +47,7 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts index 3fb9c42449..5f100dbe3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts index d64fe7af27..95332c3791 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts +++ b/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}); diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts index c23bcfed7b..a9bea410ab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html index 4d5f38d30e..ca5ae93a84 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config-dialog.component.html @@ -35,7 +35,7 @@
; private functionTypeKeys: Array; @@ -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({ diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts index b0e80973f0..a0c211f32f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.models.ts +++ b/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) => Observable>; fetchEntityKeysForDevice: (deviceId: string, types: Array) => Observable>; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts index e4f0b41500..bb1647faa4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html index 18eb6f7a23..8af7753525 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html +++ b/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" diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts index a138ed491d..b62f462576 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts +++ b/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 { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts index 41198127af..65f46240c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts +++ b/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 { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts index cea4ca85c9..ec588d69ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts +++ b/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; } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html index e029c0032d..c3490e9def 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.html @@ -18,7 +18,8 @@
{{definedDirectiveError}}
- - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts index 9e3f711ac3..ab63e0e55d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-settings.component.ts +++ b/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; 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: { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index 8dea835e24..4ca5d4302a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/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; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 8a782a14a2..16ac07e6fd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/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.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'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html new file mode 100644 index 0000000000..b15964a5af --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.html @@ -0,0 +1,74 @@ + +
+ + + +
{{ title }}
+
+
+ +
+
+
+
{{$index + 1}}
+
+ + +
+ + +
+
+
+
+ +
+
+
+
+ + + {{ 'dynamic-form.property.no-items' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.scss new file mode 100644 index 0000000000..1935e29a26 --- /dev/null +++ b/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; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-array.component.ts new file mode 100644 index 0000000000..081adbfcea --- /dev/null +++ b/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) { + 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 = []; + if (values) { + values.forEach((value) => { + const property = { + [this.itemProperty.id]: value + }; + propertiesControls.push(this.fb.control(property, [])); + }); + } + return this.fb.array(propertiesControls); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html new file mode 100644 index 0000000000..5a37c43486 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.html @@ -0,0 +1,93 @@ + +
+
+
+
dynamic-form.property.id
+
dynamic-form.property.name
+
dynamic-form.property.type
+
+ + + +
+
+
+
+ + +
+ +
+
+
+ +
+
+ +
+
+ + + {{ 'dynamic-form.property.no-properties' | translate }} + diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.scss similarity index 90% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.scss index 094ea0668b..d3bc52b22e 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.scss +++ b/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; } } diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts similarity index 60% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-properties.component.ts index 5bcdc74a8a..1c4bfefe33 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-properties.component.ts +++ b/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; + @ViewChildren(DynamicFormPropertyRowComponent) + propertyRows: QueryList; @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) { 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 = []; 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; + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html new file mode 100644 index 0000000000..5e8f9b9928 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html @@ -0,0 +1,471 @@ + +
+
{{ panelTitle | translate }}
+
+
+
dynamic-form.property.id
+ + + +
+
+
dynamic-form.property.name
+ + + +
+
+
dynamic-form.property.group-title
+ + + +
+
+
dynamic-form.property.type
+ + + + {{ formPropertyTypeTranslations.get(type) | translate }} + + + +
+
+
dynamic-form.property.array-item
+
+
dynamic-form.property.item-type
+ + + + {{ formPropertyTypeTranslations.get(type) | translate }} + + + +
+
+
dynamic-form.property.item-name
+ + + +
+ + +
+ + + + +
+ + {{ 'dynamic-form.property.value-required' | translate }} + +
+
+ + + + {{ 'dynamic-form.property.advanced-ui-settings' | translate }} + + + + +
+
dynamic-form.property.sub-label
+ + + +
+
+ + {{ 'dynamic-form.property.vertical-divider-after' | translate }} + +
+
+
+
dynamic-form.property.input-field-suffix
+ + + +
+
+
dynamic-form.property.disable-on-property
+ + + + + {{ prop }} + + + +
+
+ + +
+ + + + +
+
+
+
+
+ + +
+
+ + + +
+
dynamic-form.property.textarea-rows
+ + + +
+
+
dynamic-form.property.number-settings
+
+
dynamic-form.property.min
+ + + + +
dynamic-form.property.max
+ + + + +
dynamic-form.property.step
+ + + +
+
+
+ + + + {{ 'dynamic-form.property.properties' | translate }} + + + + + + + +
+
+ + + + {{ 'dynamic-form.property.html-section-settings' | translate }} + + + +
+ + +
+ + +
+
+
+ +
+ + + + {{ (propertyFormGroup.get('type').value === FormPropertyType.select ? 'dynamic-form.property.select-options' : 'dynamic-form.property.radio-button-options') | translate }} + + + +
+
dynamic-form.property.buttons-direction
+ + {{ 'dynamic-form.property.direction-column' | translate }} + {{ 'dynamic-form.property.direction-row' | translate }} + +
+
+ + {{ 'dynamic-form.property.enable-multiple-select' | translate }} + +
+
+ + {{ 'dynamic-form.property.allow-empty-select-option' | translate }} + +
+
+
dynamic-form.property.selected-options-limit
+
+
dynamic-form.property.min
+ + + + +
dynamic-form.property.max
+ + + +
+
+ + +
+
+
+
+
+
dynamic-form.property.help-id
+ + + +
+ +
+
dynamic-form.property.datetime-type
+ + {{ 'dynamic-form.property.datetime-type-date' | translate }} + {{ 'dynamic-form.property.datetime-type-time' | translate }} + {{ 'dynamic-form.property.datetime-type-datetime' | translate }} + +
+
+ + {{ 'dynamic-form.property.enable-clear-button' | translate }} + +
+
+
+ + dynamic-form.property.default-value + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
dynamic-form.property.default-value
+ + +
{{ item.label | customTranslate }}
+
+
+
+
+
dynamic-form.property.default-value
+ + + + + + + + + + + + + + + + + + + + + {{ item.label | customTranslate }} + + + + + {{ item.label | customTranslate }} + + + + + + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss similarity index 62% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.scss index c4b5b153d9..da613d5e61 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-panel.component.scss +++ b/ui-ngx/src/app/modules/home/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; + } + } + } + } + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts new file mode 100644 index 0000000000..960efa55fc --- /dev/null +++ b/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; + + @Output() + propertySettingsApplied = new EventEmitter(); + + 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}); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html similarity index 84% rename from ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html index bb63e69d68..95f4a41f2f 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-property-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-row.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
- -
-
- - -
-
+ +
+ +
- -
-
- - -
-
+ +
+ +
- -
-
- - -
-
+ +
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts index 03b5674268..71b30c1464 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts +++ b/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); } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts index 5d4afd93fc..eb4783b095 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.models.ts @@ -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 self that is a reference to the widget instance', - type: 'WidgetTypeInstance', - meta: 'object', - children: { - ...{ - onInit: { - description: 'The first function which is called when widget is ready for initialization.
Should be used to prepare widget DOM, process widget settings and initial subscription information.', - meta: 'function' - }, - onDataUpdated: { - description: 'Called when the new data is available from the widget subscription.
Latest data can be accessed from ' + - 'the defaultSubscription property of widget context (ctx).', - meta: 'function' - }, - onResize: { - description: 'Called when widget container is resized. Latest width and height can be obtained from widget context (ctx).', - meta: 'function' - }, - onEditModeChanged: { - description: 'Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of widget context (ctx).', - meta: 'function' - }, - onMobileModeChanged: { - description: 'Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of widget context (ctx).', - 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 Settings tab of Settings schema section.', - 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 Data key settings schema of Settings schema section.', - 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: 'WidgetTypeParameters' - } - }, - 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]: WidgetActionSource}' - } +const widgetEditorCompletions = (settingsCompletions?: TbEditorCompletions): TbEditorCompletions => { + return { + ... {self: { + description: 'Built-in variable self that is a reference to the widget instance', + type: 'WidgetTypeInstance', + meta: 'object', + children: { + ...{ + onInit: { + description: 'The first function which is called when widget is ready for initialization.
Should be used to prepare widget DOM, process widget settings and initial subscription information.', + meta: 'function' + }, + onDataUpdated: { + description: 'Called when the new data is available from the widget subscription.
Latest data can be accessed from ' + + 'the defaultSubscription property of widget context (ctx).', + meta: 'function' + }, + onResize: { + description: 'Called when widget container is resized. Latest width and height can be obtained from widget context (ctx).', + meta: 'function' + }, + onEditModeChanged: { + description: 'Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of widget context (ctx).', + meta: 'function' + }, + onMobileModeChanged: { + description: 'Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of widget context (ctx).', + 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 Settings form tab of settings section.', + meta: 'function', + return: { + description: 'An array of widget settings form properties', + type: 'Array<FormProperty>' + } + }, + getDataKeySettingsForm: { + description: 'Optional function returning particular data key settings form array as alternative to Data key settings form tab of settings section.', + meta: 'function', + return: { + description: 'An array of data key settings form properties', + type: 'Array<FormProperty>' + } + }, + getSettingsSchema: { + description: 'Deprecated. Use getSettingsForm() function.', + meta: 'function', + return: { + description: 'An widget settings schema json', + type: 'object' + } + }, + getDataKeySettingsSchema: { + description: 'Deprecated. 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: 'WidgetTypeParameters' + } + }, + 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]: WidgetActionSource}' + } + } + }, + ...widgetContextCompletionsWithSettings(settingsCompletions) } - }, - ...widgetContextCompletions - } - }} + }} + } }; -export const widgetEditorCompleter = new TbEditorCompleter(widgetEditorCompletions); +export const widgetEditorCompleter = (settingsCompletions?: TbEditorCompletions): TbEditorCompleter => { + return new TbEditorCompleter(widgetEditorCompletions(settingsCompletions)); +} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts index 046737ac10..acd48f79ab 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts +++ b/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 = (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) + }; + }) ); } }; diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts index 80438b4f4d..f76be11ba1 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts +++ b/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 ] }) diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index 647ab6113d..8e500bdbe9 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/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; - @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 ? '' : ' {'}`; diff --git a/ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts b/ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts deleted file mode 100644 index 75bcf2d1fd..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/json-form-component.models.ts +++ /dev/null @@ -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; -} diff --git a/ui-ngx/src/app/shared/components/json-form/json-form.component.html b/ui-ngx/src/app/shared/components/json-form/json-form.component.html deleted file mode 100644 index 0ed848ded5..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/json-form.component.html +++ /dev/null @@ -1,23 +0,0 @@ - -
-
-
-
diff --git a/ui-ngx/src/app/shared/components/json-form/json-form.component.ts b/ui-ngx/src/app/shared/components/json-form/json-form.component.ts deleted file mode 100644 index 76647b5bae..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/json-form.component.ts +++ /dev/null @@ -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; - - @ViewChild('reactFullscreen', {static: true}) - reactFullscreenElmRef: ElementRef; - - 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, - 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[] = [ - 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; - } -} - diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx deleted file mode 100644 index 649ee16c95..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx +++ /dev/null @@ -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; -} - -interface ThingsboardAceEditorState extends JsonFormFieldState { - isFull: boolean; - fullscreenContainerElement: Element; - helpVisible: boolean; - helpReady: boolean; - focused: boolean; -} - -class ThingsboardAceEditor extends React.Component { - - 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 = ( -
- -
-
- - { this.props.onTidy ? : null } - { this.props.form.helpId ?
- - {this.state.helpVisible ? : } - - { this.state.helpVisible && !this.state.helpReady ? -
- -
: null }
: null } - -
- Loading...
}> - - -
-
{this.props.error}
-
- ); - if (this.state.isFull) { - return ReactDOM.createPortal(formDom, this.state.fullscreenContainerElement); - } else { - return ( -
- {formDom} -
- ); - } - } -} - -export default ThingsboardBaseComponent(ThingsboardAceEditor); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-array.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-array.tsx deleted file mode 100644 index 681fe69aab..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-array.tsx +++ /dev/null @@ -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 { - - 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 = ; - } - 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( -
  • - {removeButton} - {forms} -
  • - ); - } - let addButton: JSX.Element = null; - if (!this.props.form.readonly) { - addButton = ; - } - - return ( -
    -
    -
    {this.props.form.title}
    -
      - {arrays} -
    -
    - {addButton} -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardArray); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx deleted file mode 100644 index 4e9687ce3a..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-base-component.tsx +++ /dev/null @@ -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

    - extends React.Component { - - 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 ; - } else { - return

    ; - } - } -}; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-checkbox.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-checkbox.tsx deleted file mode 100644 index fd3bac8f2c..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-checkbox.tsx +++ /dev/null @@ -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 { - render() { - return ( -
    - { - this.props.onChangeValidate(e); - }} - /> - } - label={this.props.form.title} - /> -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardCheckbox); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx deleted file mode 100644 index bf3a245663..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-color.tsx +++ /dev/null @@ -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 { - - containerRef = React.createRef(); - - 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 ( -
    -
    -
    -
    -
    - -
    - -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardColor); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx deleted file mode 100644 index 600ae8c12b..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-css.tsx +++ /dev/null @@ -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 { - - constructor(props: JsonFormFieldProps) { - super(props); - this.onTidyCss = this.onTidyCss.bind(this); - } - - onTidyCss(css: string): Observable { - return beautifyCss(css, {indent_size: 4}); - } - - render() { - return ( - - ); - } -} - -export default ThingsboardCss; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-date.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-date.tsx deleted file mode 100644 index 3b253cfa8e..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-date.tsx +++ /dev/null @@ -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 { - - 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 ( - -
    - - -
    -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardDate); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-fieldset.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-fieldset.tsx deleted file mode 100644 index 31f09fc409..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-fieldset.tsx +++ /dev/null @@ -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 { - - 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 ( -
    -
    - {this.props.form.title} -
    -
    - {forms} -
    -
    - ); - } -} - -export default ThingsboardFieldSet; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx deleted file mode 100644 index f267154182..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-html.tsx +++ /dev/null @@ -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 { - - constructor(props: JsonFormFieldProps) { - super(props); - this.onTidyHtml = this.onTidyHtml.bind(this); - } - - onTidyHtml(html: string): Observable { - return beautifyHtml(html, {indent_size: 4}); - } - - render() { - return ( - - ); - } -} - -export default ThingsboardHtml; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-icon.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-icon.tsx deleted file mode 100644 index 00366b86e8..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-icon.tsx +++ /dev/null @@ -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 { - - containerRef = React.createRef(); - - 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 ( -
    -
    - - {pickedIcon} - - -
    - -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardIcon); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx deleted file mode 100644 index 2a03d95669..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-image.tsx +++ /dev/null @@ -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 { - - 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 = ; - } else { - previewComponent =
    No image selected
    ; - } - - return ( -
    - -
    -
    {previewComponent}
    -
    - - - -
    - - {({getRootProps, getInputProps}) => ( -
    -
    Drop an image or click to select a file to upload.
    - -
    - )} -
    -
    -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardImage); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx deleted file mode 100644 index 74d01137a8..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-javascript.tsx +++ /dev/null @@ -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 { - - constructor(props: JsonFormFieldProps) { - super(props); - this.onTidyJavascript = this.onTidyJavascript.bind(this); - } - - onTidyJavascript(javascript: string): Observable { - return beautifyJs(javascript, {indent_size: 4, wrap_line_length: 60}); - } - - render() { - return ( - - ); - } -} - -export default ThingsboardJavaScript; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx deleted file mode 100644 index 9ab839616b..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-json.tsx +++ /dev/null @@ -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 { - - constructor(props: JsonFormFieldProps) { - super(props); - this.onTidyJson = this.onTidyJson.bind(this); - } - - onTidyJson(json: string): Observable { - return beautifyJs(json, {indent_size: 4}); - } - - render() { - return ( - - ); - } -} - -export default ThingsboardJson; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-number.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-number.tsx deleted file mode 100644 index 4616324de2..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-number.tsx +++ /dev/null @@ -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 { - - 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) { - 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 ( -
    - -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardNumber); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx deleted file mode 100644 index 99031ff480..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx +++ /dev/null @@ -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 { - render() { - const items = this.props.form.titleMap.map((item, index) => { - return ( - } label={item.name} key={index} /> - ); - }); - - let row = false; - if (this.props.form.direction === 'row') { - row = true; - } - - return ( - - {this.props.form.title} - { - this.props.onChangeValidate(e); - }}> - {items} - - - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardRadios); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx deleted file mode 100644 index ee21674b94..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx +++ /dev/null @@ -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; - focused: boolean; -} - -class ThingsboardRcSelect extends React.Component { - - 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) => ( - - )); - } - - 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 ( -
    - - -
    {this.props.error}
    -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardRcSelect); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-react.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-react.tsx deleted file mode 100644 index a56054a614..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-react.tsx +++ /dev/null @@ -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 { - - static defaultProps: JsonFormProps; - - constructor(props) { - super(props); - } - - render() { - if (this.props.form.length > 0) { - return ; - } else { - return
    ; - } - } -} - -ReactSchemaForm.defaultProps = { - isFullscreen: false, - schema: {}, - form: ['*'], - groupInfoes: [], - option: { - formDefaults: { - startEmpty: true - } - } -}; - -export default ReactSchemaForm; diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx deleted file mode 100644 index bbdd90cbe7..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-schema-form.tsx +++ /dev/null @@ -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 { - - 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 ; - } - - 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 ( -
    {forms}
    - ); - } - - 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 = ; - content.push(item); - } - return (
    {content}
    ); - } 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 { - 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 (
    -
    {this.props.info.GroupTitle}
    -
    {this.props.forms}
    -
    ); - } -} diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-select.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-select.tsx deleted file mode 100644 index 46da881e38..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-select.tsx +++ /dev/null @@ -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 { - - 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) { - - this.setState({ - currentValue: event.target.value - }); - this.props.onChangeValidate(event); - } - - render() { - const menuItems = this.props.form.titleMap.map((item, idx) => ( - {item.name} - )); - - return ( - - {this.props.form.title} - - - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardSelect); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-text.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-text.tsx deleted file mode 100644 index 6fad3f3480..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-text.tsx +++ /dev/null @@ -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 { - - 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 ( -
    - { - 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'}}/> -
    - ); - } -} - -export default ThingsboardBaseComponent(ThingsboardText); diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts b/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts deleted file mode 100644 index ffcf201136..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form.models.ts +++ /dev/null @@ -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 | Array; - tabs?: Array; - 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; -} diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form.scss b/ui-ngx/src/app/shared/components/json-form/react/json-form.scss deleted file mode 100644 index 42e1dc9677..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form.scss +++ /dev/null @@ -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; - } - } - } - } -} diff --git a/ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts b/ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts deleted file mode 100644 index ab3497fd53..0000000000 --- a/ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts +++ /dev/null @@ -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({}, 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; diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index 375f22e02f..eb4c15fc18 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -15,7 +15,8 @@ limitations under the License. --> - diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index 951abaa12b..4121a7e8d7 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/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[]; diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.html b/ui-ngx/src/app/shared/components/time/datetime.component.html index edf59702f1..f0965deaf8 100644 --- a/ui-ngx/src/app/shared/components/time/datetime.component.html +++ b/ui-ngx/src/app/shared/components/time/datetime.component.html @@ -15,26 +15,26 @@ limitations under the License. --> -
    - - {{ dateText | translate }} - - - - - - {{ timeText | translate }} - - - - -
    + + {{ dateText }} +
    + + +
    + + +
    diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.scss b/ui-ngx/src/app/shared/components/time/datetime.component.scss new file mode 100644 index 0000000000..b3333d34d5 --- /dev/null +++ b/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); + } +} diff --git a/ui-ngx/src/app/shared/components/time/datetime.component.ts b/ui-ngx/src/app/shared/components/time/datetime.component.ts index c004ab3786..52029fb958 100644 --- a/ui-ngx/src/app/shared/components/time/datetime.component.ts +++ b/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); + } + } diff --git a/ui-ngx/src/app/shared/components/value-input.component.html b/ui-ngx/src/app/shared/components/value-input.component.html index e4aa511e83..e6138dd321 100644 --- a/ui-ngx/src/app/shared/components/value-input.component.html +++ b/ui-ngx/src/app/shared/components/value-input.component.html @@ -68,11 +68,16 @@ warning
    - - {{ trueLabel }} - {{ falseLabel }} - + + + {{ trueLabel }} + {{ falseLabel }} + + + {{ modelValue ? trueLabel : falseLabel }} + +
    -
    +

    {{ importTitle }}

    @@ -30,15 +30,28 @@
    - + {{ importFileLabel | translate }} + {{ importContentLabel | translate }} + + + +
    diff --git a/ui-ngx/src/app/shared/import-export/import-dialog.component.ts b/ui-ngx/src/app/shared/import-export/import-dialog.component.ts index b68fe3b2cd..e4256229c4 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog.component.ts +++ b/ui-ngx/src/app/shared/import-export/import-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; +import { Component, DestroyRef, Inject, OnInit, SkipSelf } from '@angular/core'; import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; @@ -23,10 +23,14 @@ import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDire import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { isDefinedAndNotNull } from '@core/utils'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export interface ImportDialogData { importTitle: string; importFileLabel: string; + enableImportFromContent?: boolean; + importContentLabel?: string; } @Component({ @@ -40,9 +44,13 @@ export class ImportDialogComponent extends DialogComponent, @@ -50,17 +58,26 @@ export class ImportDialogComponent extends DialogComponent, + private destroyRef: DestroyRef, private fb: UntypedFormBuilder) { super(store, router, dialogRef); this.importTitle = data.importTitle; this.importFileLabel = data.importFileLabel; + this.enableImportFromContent = isDefinedAndNotNull(data.enableImportFromContent) ? data.enableImportFromContent : false; + this.importContentLabel = data.importContentLabel; + } + ngOnInit(): void { this.importFormGroup = this.fb.group({ + importType: ['file'], + fileContent: [null, [Validators.required]], jsonContent: [null, [Validators.required]] }); - } - - ngOnInit(): void { + this.importFormGroup.get('importType').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.importTypeChanged(); + }); } isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { @@ -85,7 +102,19 @@ export class ImportDialogComponent extends DialogComponent, isSingleWidget: boolean, customTitle: string, missingEntityAliases: EntityAliases) => Observable; @@ -119,6 +120,27 @@ export class ImportExportService { } + public exportFormProperties(properties: FormProperty[], fileName: string) { + this.exportToPc(properties, fileName); + } + + public importFormProperties(): Observable { + return this.openImportDialog('dynamic-form.import-form', + 'dynamic-form.json-file', true, 'dynamic-form.json-content').pipe( + map((properties: FormProperty[]) => { + if (!this.validateImportedFormProperties(properties)) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('dynamic-form.invalid-form-json-file-error'), + type: 'error'})); + throw new Error('Invalid form JSON file'); + } else { + return properties; + } + }), + catchError(() => of(null)) + ); + } + public exportImage(type: ImageResourceType, key: string) { this.imageService.exportImage(type, key).subscribe( { @@ -927,6 +949,14 @@ export class ImportExportService { type: 'error'})); } + private validateImportedFormProperties(properties: FormProperty[]): boolean { + if (!properties.length) { + return false; + } else { + return !properties.some(p => !propertyValid(p)); + } + } + private validateImportedImage(image: ImageExportData): boolean { return !(!isNotEmptyStr(image.data) || !isNotEmptyStr(image.title) @@ -1105,14 +1135,17 @@ export class ImportExportService { }; } - private openImportDialog(importTitle: string, importFileLabel: string): Observable { + private openImportDialog(importTitle: string, importFileLabel: string, + enableImportFromContent = false, importContentLabel?: string): Observable { return this.dialog.open(ImportDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { importTitle, - importFileLabel + importFileLabel, + enableImportFromContent, + importContentLabel } }).afterClosed().pipe( map((importedData) => { diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts b/ui-ngx/src/app/shared/legacy/json-form-utils.ts similarity index 70% rename from ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts rename to ui-ngx/src/app/shared/legacy/json-form-utils.ts index d75819b73d..87e0afb31c 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-utils.ts +++ b/ui-ngx/src/app/shared/legacy/json-form-utils.ts @@ -14,63 +14,12 @@ /// limitations under the License. /// -import * as tv from 'tv4'; import ObjectPath from 'objectpath'; import _ from 'lodash'; import { DefaultsFormOptions, - FormOption, - JsonFormData, - JsonSchemaData, - SchemaValidationResult + FormOption } from './json-form.models'; -import { isDefined, isEqual, isString, isUndefined } from '@core/utils'; - -function validateBySchema(schema: any, value: any): SchemaValidationResult { - return tv.validateResult(value, schema); -} - -function validate(form: any, value: any): SchemaValidationResult { - - if (!form) { - return {valid: true}; - } - const schema = form.schema; - - if (!schema) { - return {valid: true}; - } - - if (value === '') { - value = undefined; - } - - // Numbers fields will give a null value, which also means empty field - if (form.type === 'number' && value === null) { - value = undefined; - } - - if (form.type === 'number' && isNaN(parseFloat(value))) { - value = undefined; - } - const wrap: any = {type: 'object', properties: {}}; - const propName = form.key[form.key.length - 1]; - wrap.properties[propName] = schema; - - if (form.required) { - wrap.required = [propName]; - } - const valueWrap = {}; - if (typeof value !== 'undefined') { - valueWrap[propName] = value; - } - - const tv4Result: SchemaValidationResult = tv.validateResult(valueWrap, wrap); - if (tv4Result != null && !tv4Result.valid && form.validationMessage != null && typeof value !== 'undefined') { - tv4Result.error.message = form.validationMessage; - } - return tv4Result; -} function stripNullType(type: any): string { if (Array.isArray(type) && type.length === 2) { @@ -438,155 +387,7 @@ function merge(schema: any, form: any[], ignore: { [key: string]: boolean }, opt })); } -function selectOrSet(projection: string | (string | number)[], obj: any, valueToSet?: any): any { - const numRe = /^\d+$/; - - if (!obj) { - obj = this; - } - const parts = typeof projection === 'string' ? ObjectPath.parse(projection) : projection; - - if (typeof valueToSet !== 'undefined' && parts.length === 1) { - obj[parts[0]] = valueToSet; - return obj; - } - - if (typeof valueToSet !== 'undefined' && typeof obj[parts[0]] === 'undefined') { - obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {}; - } - - let value = obj[parts[0]]; - for (let i = 1; i < parts.length; i++) { - if (parts[i] === '') { - return undefined; - } - if (typeof valueToSet !== 'undefined') { - if (i === parts.length - 1) { - value[parts[i]] = valueToSet; - return valueToSet; - } else { - let tmp = value[parts[i]]; - if (typeof tmp === 'undefined' || tmp === null) { - tmp = numRe.test(parts[i + 1]) ? [] : {}; - value[parts[i]] = tmp; - } - value = tmp; - } - } else if (value) { - value = value[parts[i]]; - } - } - return value; -} - -function updateValue(projection: string | (string | number)[], obj: any, valueToSet: any): boolean { - const numRe = /^\d+$/; - - if (!obj) { - obj = this; - } - - if (!obj) { - return false; - } - - const parts: string[] = isString(projection) ? ObjectPath.parse(projection) : projection; - - if (parts.length === 1) { - return setValue(obj, parts[0], valueToSet); - } - - if (isUndefined(obj[parts[0]])) { - obj[parts[0]] = parts.length > 2 && numRe.test(parts[1]) ? [] : {}; - } - - let value = obj[parts[0]]; - for (let i = 1; i < parts.length; i++) { - if (parts[i] === '') { - return false; - } - if (i === parts.length - 1) { - return setValue(value, parts[i], valueToSet); - } else { - let tmp = value[parts[i]]; - if (isUndefined(tmp) || tmp === null) { - tmp = numRe.test(parts[i + 1]) ? [] : {}; - value[parts[i]] = tmp; - } - value = tmp; - } - } - return value; -} - - -function setValue(obj: any, key: string, val: any): boolean { - let changed = false; - if (obj) { - if (isUndefined(val)) { - if (isDefined(obj[key])) { - delete obj[key]; - changed = true; - } - } else { - changed = !isEqual(obj[key], val); - obj[key] = val; - } - } - return changed; -} - -function traverseSchema(schema: JsonSchemaData, fn: (prop: any, path: string[]) => any, path?: string[], ignoreArrays?: boolean) { - ignoreArrays = typeof ignoreArrays !== 'undefined' ? ignoreArrays : true; - - path = path || []; - - const traverse = ($schema: JsonSchemaData, $fn: (prop: any, path: string[]) => any, $path: string[]) => { - $fn($schema, $path); - if ($schema.properties) { - for (const k of Object.keys($schema.properties)) { - if ($schema.properties.hasOwnProperty(k)) { - const currentPath = $path.slice(); - currentPath.push(k); - traverse($schema.properties[k], $fn, currentPath); - } - } - } - if (!ignoreArrays && $schema.items) { - const arrPath = $path.slice(); - arrPath.push(''); - traverse($schema.items, $fn, arrPath); - } - }; - - traverse(schema, fn, path || []); -} - -function traverseForm(form: JsonFormData, fn: (form: JsonFormData) => any) { - fn(form); - if (form.items) { - form.items.forEach((f) => { - traverseForm(f, fn); - }); - } - - if (form.tabs) { - form.tabs.forEach((tab) => { - tab.items.forEach((f) => { - traverseForm(f, fn); - }); - }); - } -} - - const utils = { - validateBySchema, - validate, - merge, - updateValue, - selectOrSet, - traverseSchema, - traverseForm + merge }; export default utils; diff --git a/ui-ngx/src/app/shared/legacy/json-form.models.ts b/ui-ngx/src/app/shared/legacy/json-form.models.ts new file mode 100644 index 0000000000..ffd2830133 --- /dev/null +++ b/ui-ngx/src/app/shared/legacy/json-form.models.ts @@ -0,0 +1,88 @@ +/// +/// 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. +/// + +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 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 | Array; + tabs?: Array; + tags?: any; + helpId?: string; + startEmpty?: boolean; + [key: string]: any; +} + +export interface GroupInfo { + formIndex: number; + GroupTitle: string; +} + +export interface JsonSchema { + type: string; + title?: string; + properties: {[key: string]: any}; + required?: string[]; +} + +export interface JsonSettingsSchema { + schema?: JsonSchema; + form?: any[]; + groupInfoes?: GroupInfo[]; +} diff --git a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts index b2b0b6235d..d6a2c0a25f 100644 --- a/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts +++ b/ui-ngx/src/app/shared/models/ace/widget-completion.models.ts @@ -128,614 +128,620 @@ export const timewindowCompletion: TbEditorCompletion = { } }; -export const widgetContextCompletions: TbEditorCompletions = { - ctx: { - description: 'A reference to widget context that has all necessary API
    and data used by widget instance.', - meta: 'object', - type: 'WidgetContext', - children: { - ...{ - $container: { - description: 'Container element of the widget.
    Can be used to dynamically access or modify widget DOM using jQuery API.', - meta: 'property', - type: 'jQuery Object' - }, - $scope: { - description: 'Reference to the current widget component.
    Can be used to access/modify component properties when widget is built using Angular approach.', - meta: 'property', - type: 'IDynamicWidgetComponent' - }, - width: { - description: 'Current width of widget container in pixels.', - meta: 'property', - type: 'number' - }, - height: { - description: 'Current height of widget container in pixels.', - meta: 'property', - type: 'number' - }, - isEdit: { - description: 'Indicates whether the dashboard is in in the view or editing state.', - meta: 'property', - type: 'boolean' - }, - isMobile: { - description: 'Indicates whether the dashboard view is less then 960px width (default mobile breakpoint).', - meta: 'property', - type: 'boolean' - }, - widgetConfig: { - description: 'Common widget configuration containing properties such as color (text color), backgroundColor (widget background color), etc.', - meta: 'property', - type: 'WidgetConfig', - children: { - title: { - description: 'Widget title.', - meta: 'property', - type: 'string' - }, - titleIcon: { - description: 'Widget title icon.', - meta: 'property', - type: 'string' - }, - showTitle: { - description: 'Whether to show widget title.', - meta: 'property', - type: 'boolean' - }, - showTitleIcon: { - description: 'Whether to show widget title icon.', - meta: 'property', - type: 'boolean' - }, - iconColor: { - description: 'Widget title icon color.', - meta: 'property', - type: 'string' - }, - iconSize: { - description: 'Widget title icon size.', - meta: 'property', - type: 'string' - }, - titleTooltip: { - description: 'Widget title tooltip content.', - meta: 'property', - type: 'string' - }, - dropShadow: { - description: 'Enable/disable widget card shadow.', - meta: 'property', - type: 'boolean' - }, - enableFullscreen: { - description: 'Whether to enable fullscreen button on widget.', - meta: 'property', - type: 'boolean' - }, - useDashboardTimewindow: { - description: 'Whether to use dashboard timewindow (applicable for timeseries widgets).', - meta: 'property', - type: 'boolean' - }, - displayTimewindow: { - description: 'Whether to display timewindow (applicable for timeseries widgets).', - meta: 'property', - type: 'boolean' - }, - showLegend: { - description: 'Whether to show legend.', - meta: 'property', - type: 'boolean' - }, - legendConfig: { - description: 'Legend configuration.', - meta: 'property', - type: 'LegendConfig', - children: { - position: { - description: 'Legend position. Possible values: \'top\', \'bottom\', \'left\', \'right\'', - meta: 'property', - type: 'LegendPosition', - }, - direction: { - description: 'Legend direction. Possible values: \'column\', \'row\'', - meta: 'property', - type: 'LegendDirection', - }, - showMin: { - description: 'Whether to display aggregated min values.', - meta: 'property', - type: 'boolean', - }, - showMax: { - description: 'Whether to display aggregated max values.', - meta: 'property', - type: 'boolean', - }, - showAvg: { - description: 'Whether to display aggregated average values.', - meta: 'property', - type: 'boolean', - }, - showTotal: { - description: 'Whether to display aggregated total values.', - meta: 'property', - type: 'boolean', +export const widgetContextCompletionsWithSettings = (settingsCompletions?: TbEditorCompletions): TbEditorCompletions => { + return { + ctx: { + description: 'A reference to widget context that has all necessary API
    and data used by widget instance.', + meta: 'object', + type: 'WidgetContext', + children: { + ...{ + $container: { + description: 'Container element of the widget.
    Can be used to dynamically access or modify widget DOM using jQuery API.', + meta: 'property', + type: 'jQuery Object' + }, + $scope: { + description: 'Reference to the current widget component.
    Can be used to access/modify component properties when widget is built using Angular approach.', + meta: 'property', + type: 'IDynamicWidgetComponent' + }, + width: { + description: 'Current width of widget container in pixels.', + meta: 'property', + type: 'number' + }, + height: { + description: 'Current height of widget container in pixels.', + meta: 'property', + type: 'number' + }, + isEdit: { + description: 'Indicates whether the dashboard is in in the view or editing state.', + meta: 'property', + type: 'boolean' + }, + isMobile: { + description: 'Indicates whether the dashboard view is less then 960px width (default mobile breakpoint).', + meta: 'property', + type: 'boolean' + }, + widgetConfig: { + description: 'Common widget configuration containing properties such as color (text color), backgroundColor (widget background color), etc.', + meta: 'property', + type: 'WidgetConfig', + children: { + title: { + description: 'Widget title.', + meta: 'property', + type: 'string' + }, + titleIcon: { + description: 'Widget title icon.', + meta: 'property', + type: 'string' + }, + showTitle: { + description: 'Whether to show widget title.', + meta: 'property', + type: 'boolean' + }, + showTitleIcon: { + description: 'Whether to show widget title icon.', + meta: 'property', + type: 'boolean' + }, + iconColor: { + description: 'Widget title icon color.', + meta: 'property', + type: 'string' + }, + iconSize: { + description: 'Widget title icon size.', + meta: 'property', + type: 'string' + }, + titleTooltip: { + description: 'Widget title tooltip content.', + meta: 'property', + type: 'string' + }, + dropShadow: { + description: 'Enable/disable widget card shadow.', + meta: 'property', + type: 'boolean' + }, + enableFullscreen: { + description: 'Whether to enable fullscreen button on widget.', + meta: 'property', + type: 'boolean' + }, + useDashboardTimewindow: { + description: 'Whether to use dashboard timewindow (applicable for timeseries widgets).', + meta: 'property', + type: 'boolean' + }, + displayTimewindow: { + description: 'Whether to display timewindow (applicable for timeseries widgets).', + meta: 'property', + type: 'boolean' + }, + showLegend: { + description: 'Whether to show legend.', + meta: 'property', + type: 'boolean' + }, + legendConfig: { + description: 'Legend configuration.', + meta: 'property', + type: 'LegendConfig', + children: { + position: { + description: 'Legend position. Possible values: \'top\', \'bottom\', \'left\', \'right\'', + meta: 'property', + type: 'LegendPosition', + }, + direction: { + description: 'Legend direction. Possible values: \'column\', \'row\'', + meta: 'property', + type: 'LegendDirection', + }, + showMin: { + description: 'Whether to display aggregated min values.', + meta: 'property', + type: 'boolean', + }, + showMax: { + description: 'Whether to display aggregated max values.', + meta: 'property', + type: 'boolean', + }, + showAvg: { + description: 'Whether to display aggregated average values.', + meta: 'property', + type: 'boolean', + }, + showTotal: { + description: 'Whether to display aggregated total values.', + meta: 'property', + type: 'boolean', + } } + }, + timewindow: timewindowCompletion, + mobileHeight: { + description: 'Widget height in mobile mode.', + meta: 'property', + type: 'number' + }, + mobileOrder: { + description: 'Widget order in mobile mode.', + meta: 'property', + type: 'number' + }, + color: { + description: 'Widget text color.', + meta: 'property', + type: 'string' + }, + backgroundColor: { + description: 'Widget background color.', + meta: 'property', + type: 'string' + }, + padding: { + description: 'Widget card padding.', + meta: 'property', + type: 'string' + }, + margin: { + description: 'Widget card margin.', + meta: 'property', + type: 'string' + }, + widgetStyle: { + description: 'Widget element style object.', + meta: 'property', + type: 'object' + }, + titleStyle: { + description: 'Widget title element style object.', + meta: 'property', + type: 'object' + }, + units: { + description: 'Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.', + meta: 'property', + type: 'string' + }, + decimals: { + description: 'Optional property defining how many positions should be used to display decimal part of the value number.', + meta: 'property', + type: 'number' + }, + actions: { + description: 'Map of configured widget actions.', + meta: 'property', + type: 'object' + }, + settings: { + description: 'Widget settings containing widget specific properties according to the defined settings form.', + meta: 'property', + type: 'object', + children: settingsCompletions + }, + alarmSource: { + description: 'Configured alarm source for alarm widget type.', + meta: 'property', + type: 'Datasource' + }, + alarmSearchStatus: { + description: 'Configured default alarm search status for alarm widget type.', + meta: 'property', + type: 'AlarmSearchStatus' + }, + alarmsPollingInterval: { + description: 'Configured alarms polling interval for alarm widget type.', + meta: 'property', + type: 'number' + }, + alarmsMaxCountLoad: { + description: 'Configured maximum alarms to load for alarm widget type.', + meta: 'property', + type: 'number' + }, + alarmsFetchSize: { + description: 'Configured alarms page size used to load alarms.', + meta: 'property', + type: 'number' + }, + datasources: { + description: 'Array of configured widget datasources.', + meta: 'property', + type: 'Array<Datasource>' } - }, - timewindow: timewindowCompletion, - mobileHeight: { - description: 'Widget height in mobile mode.', - meta: 'property', - type: 'number' - }, - mobileOrder: { - description: 'Widget order in mobile mode.', - meta: 'property', - type: 'number' - }, - color: { - description: 'Widget text color.', - meta: 'property', - type: 'string' - }, - backgroundColor: { - description: 'Widget background color.', - meta: 'property', - type: 'string' - }, - padding: { - description: 'Widget card padding.', - meta: 'property', - type: 'string' - }, - margin: { - description: 'Widget card margin.', - meta: 'property', - type: 'string' - }, - widgetStyle: { - description: 'Widget element style object.', - meta: 'property', - type: 'object' - }, - titleStyle: { - description: 'Widget title element style object.', - meta: 'property', - type: 'object' - }, - units: { - description: 'Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.', - meta: 'property', - type: 'string' - }, - decimals: { - description: 'Optional property defining how many positions should be used to display decimal part of the value number.', - meta: 'property', - type: 'number' - }, - actions: { - description: 'Map of configured widget actions.', - meta: 'property', - type: 'object' - }, - settings: { - description: 'Object holding widget settings according to widget type.', - meta: 'property', - type: 'object' - }, - alarmSource: { - description: 'Configured alarm source for alarm widget type.', - meta: 'property', - type: 'Datasource' - }, - alarmSearchStatus: { - description: 'Configured default alarm search status for alarm widget type.', - meta: 'property', - type: 'AlarmSearchStatus' - }, - alarmsPollingInterval: { - description: 'Configured alarms polling interval for alarm widget type.', - meta: 'property', - type: 'number' - }, - alarmsMaxCountLoad: { - description: 'Configured maximum alarms to load for alarm widget type.', - meta: 'property', - type: 'number' - }, - alarmsFetchSize: { - description: 'Configured alarms page size used to load alarms.', - meta: 'property', - type: 'number' - }, - datasources: { - description: 'Array of configured widget datasources.', - meta: 'property', - type: 'Array<Datasource>' } - } - }, - settings: { - description: 'Widget settings containing widget specific properties according to the defined settings json schema', - meta: 'property', - type: 'object' - }, - datasources: { - description: 'Array of resolved widget datasources.', - meta: 'property', - type: 'Array<Datasource>' - }, - data: { - description: 'Array of latest datasources data.', - meta: 'property', - type: 'Array<DatasourceData>' - }, - timeWindow: { - description: 'Current widget timewindow (applicable for timeseries widgets).', - meta: 'property', - type: 'WidgetTimewindow' - }, - units: { - description: 'Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.', - meta: 'property', - type: 'string' - }, - decimals: { - description: 'Optional property defining how many positions should be used to display decimal part of the value number.', - meta: 'property', - type: 'number' - }, - currentUser: { - description: 'Current user object.', - meta: 'property', - type: 'AuthUser', - children: { - sub: { - description: 'User subject (email).', - meta: 'property', - type: 'string' - }, - scopes: { - description: 'User security scopes.', - meta: 'property', - type: 'Array' - }, - userId: { - description: 'User id.', - meta: 'property', - type: 'string' - }, - firstName: { - description: 'User first name.', - meta: 'property', - type: 'string' - }, - lastName: { - description: 'User last name.', - meta: 'property', - type: 'string' - }, - enabled: { - description: 'Whether is user enabled.', - meta: 'property', - type: 'boolean' - }, - tenantId: { - description: 'Tenant id of the user.', - meta: 'property', - type: 'string' - }, - customerId: { - description: 'Customer id of the user (available when user belongs to specific customer).', - meta: 'property', - type: 'string' - }, - isPublic: { - description: 'Special flag indicating public user.', - meta: 'property', - type: 'boolean' - }, - authority: { - description: 'User authority. Possible values: SYS_ADMIN, TENANT_ADMIN, CUSTOMER_USER', - meta: 'property', - type: 'Authority' + }, + settings: { + description: 'Widget settings containing widget specific properties according to the defined settings form.', + meta: 'property', + type: 'object', + children: settingsCompletions + }, + datasources: { + description: 'Array of resolved widget datasources.', + meta: 'property', + type: 'Array<Datasource>' + }, + data: { + description: 'Array of latest datasources data.', + meta: 'property', + type: 'Array<DatasourceData>' + }, + timeWindow: { + description: 'Current widget timewindow (applicable for timeseries widgets).', + meta: 'property', + type: 'WidgetTimewindow' + }, + units: { + description: 'Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.', + meta: 'property', + type: 'string' + }, + decimals: { + description: 'Optional property defining how many positions should be used to display decimal part of the value number.', + meta: 'property', + type: 'number' + }, + currentUser: { + description: 'Current user object.', + meta: 'property', + type: 'AuthUser', + children: { + sub: { + description: 'User subject (email).', + meta: 'property', + type: 'string' + }, + scopes: { + description: 'User security scopes.', + meta: 'property', + type: 'Array' + }, + userId: { + description: 'User id.', + meta: 'property', + type: 'string' + }, + firstName: { + description: 'User first name.', + meta: 'property', + type: 'string' + }, + lastName: { + description: 'User last name.', + meta: 'property', + type: 'string' + }, + enabled: { + description: 'Whether is user enabled.', + meta: 'property', + type: 'boolean' + }, + tenantId: { + description: 'Tenant id of the user.', + meta: 'property', + type: 'string' + }, + customerId: { + description: 'Customer id of the user (available when user belongs to specific customer).', + meta: 'property', + type: 'string' + }, + isPublic: { + description: 'Special flag indicating public user.', + meta: 'property', + type: 'boolean' + }, + authority: { + description: 'User authority. Possible values: SYS_ADMIN, TENANT_ADMIN, CUSTOMER_USER', + meta: 'property', + type: 'Authority' + } } - } - }, - hideTitlePanel: { - description: 'Manages visibility of widget title panel. Useful for widget with custom title panels or different states. updateWidgetParams() function must be called after this property change.', - meta: 'property', - type: 'boolean' - }, - widgetTitle: { - description: 'If set, will override configured widget title text. updateWidgetParams() function must be called after this property change.', - meta: 'property', - type: 'string' - }, - detectChanges: { - description: 'Trigger change detection for current widget. Must be invoked when widget HTML template bindings should be updated due to widget data changes.', - meta: 'function' - }, - updateWidgetParams: { - description: 'Updates widget with runtime set properties such as widgetTitle, hideTitlePanel, etc. Must be invoked in order these properties changes take effect.', - meta: 'function' - }, - defaultSubscription: { - description: 'Default widget subscription object contains all subscription information,
    including current data, according to the widget type.', - meta: 'property', - type: 'IWidgetSubscription' - }, - timewindowFunctions: { - description: 'Object with timewindow functions used to manage widget data time frame. Can by used by Time-series or Alarm widgets.', - meta: 'property', - type: 'TimewindowFunctions', - children: { - onUpdateTimewindow: { - description: 'This function can be used to update current subscription time frame
    to historical one identified by startTimeMs and endTimeMs arguments.', - meta: 'function', - args: [ - { - name: 'startTimeMs', - description: 'Timewindow start time in UTC milliseconds', - type: 'number' - }, - { - name: 'endTimeMs', - description: 'Timewindow end time in UTC milliseconds', - type: 'number' - } - ] - }, - onResetTimewindow: { - description: 'Resets subscription time frame to default defined by widget timewindow component
    or dashboard timewindow depending on widget settings.', - meta: 'function' + }, + hideTitlePanel: { + description: 'Manages visibility of widget title panel. Useful for widget with custom title panels or different states. updateWidgetParams() function must be called after this property change.', + meta: 'property', + type: 'boolean' + }, + widgetTitle: { + description: 'If set, will override configured widget title text. updateWidgetParams() function must be called after this property change.', + meta: 'property', + type: 'string' + }, + detectChanges: { + description: 'Trigger change detection for current widget. Must be invoked when widget HTML template bindings should be updated due to widget data changes.', + meta: 'function' + }, + updateWidgetParams: { + description: 'Updates widget with runtime set properties such as widgetTitle, hideTitlePanel, etc. Must be invoked in order these properties changes take effect.', + meta: 'function' + }, + defaultSubscription: { + description: 'Default widget subscription object contains all subscription information,
    including current data, according to the widget type.', + meta: 'property', + type: 'IWidgetSubscription' + }, + timewindowFunctions: { + description: 'Object with timewindow functions used to manage widget data time frame. Can by used by Time-series or Alarm widgets.', + meta: 'property', + type: 'TimewindowFunctions', + children: { + onUpdateTimewindow: { + description: 'This function can be used to update current subscription time frame
    to historical one identified by startTimeMs and endTimeMs arguments.', + meta: 'function', + args: [ + { + name: 'startTimeMs', + description: 'Timewindow start time in UTC milliseconds', + type: 'number' + }, + { + name: 'endTimeMs', + description: 'Timewindow end time in UTC milliseconds', + type: 'number' + } + ] + }, + onResetTimewindow: { + description: 'Resets subscription time frame to default defined by widget timewindow component
    or dashboard timewindow depending on widget settings.', + meta: 'function' + } } - } - }, - controlApi: { - description: 'Object that provides API functions for RPC (Control) widgets.', - meta: 'property', - type: 'RpcApi', - children: { - sendOneWayCommand: { - description: 'Sends one way (without response) RPC command to the device.', - meta: 'function', - args: [ - { - name: 'method', - description: 'RPC method name', - type: 'string' - }, - { - name: 'params', - description: 'RPC method params, custom json object', - type: 'object', - optional: true - }, - { - name: 'timeout', - description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.', - type: 'number', - optional: true - }, - { - name: 'persistent', - description: 'RPC request persistent', - type: 'boolean', - optional: true - }, - { - name: 'persistentPollingInterval', - description: 'Polling interval in milliseconds to get persistent RPC command response', - type: 'number', - optional: true + }, + controlApi: { + description: 'Object that provides API functions for RPC (Control) widgets.', + meta: 'property', + type: 'RpcApi', + children: { + sendOneWayCommand: { + description: 'Sends one way (without response) RPC command to the device.', + meta: 'function', + args: [ + { + name: 'method', + description: 'RPC method name', + type: 'string' + }, + { + name: 'params', + description: 'RPC method params, custom json object', + type: 'object', + optional: true + }, + { + name: 'timeout', + description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.', + type: 'number', + optional: true + }, + { + name: 'persistent', + description: 'RPC request persistent', + type: 'boolean', + optional: true + }, + { + name: 'persistentPollingInterval', + description: 'Polling interval in milliseconds to get persistent RPC command response', + type: 'number', + optional: true + } + ], + return: { + description: 'A command execution Observable.', + type: 'Observable<any>' } - ], - return: { - description: 'A command execution Observable.', - type: 'Observable<any>' - } - }, - sendTwoWayCommand: { - description: 'Sends two way (with response) RPC command to the device.', - meta: 'function', - args: [ - { - name: 'method', - description: 'RPC method name', - type: 'string' - }, - { - name: 'params', - description: 'RPC method params, custom json object', - type: 'object', - optional: true - }, - { - name: 'timeout', - description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.', - type: 'number', - optional: true - }, - { - name: 'persistent', - description: 'RPC request persistent', - type: 'boolean', - optional: true - }, - { - name: 'persistentPollingInterval', - description: 'Polling interval in milliseconds to get persistent RPC command response', - type: 'number', - optional: true + }, + sendTwoWayCommand: { + description: 'Sends two way (with response) RPC command to the device.', + meta: 'function', + args: [ + { + name: 'method', + description: 'RPC method name', + type: 'string' + }, + { + name: 'params', + description: 'RPC method params, custom json object', + type: 'object', + optional: true + }, + { + name: 'timeout', + description: 'Maximum delay in milliseconds to wait until response/acknowledgement is received.', + type: 'number', + optional: true + }, + { + name: 'persistent', + description: 'RPC request persistent', + type: 'boolean', + optional: true + }, + { + name: 'persistentPollingInterval', + description: 'Polling interval in milliseconds to get persistent RPC command response', + type: 'number', + optional: true + } + ], + return: { + description: 'A command execution Observable of response body.', + type: 'Observable<any>' } - ], - return: { - description: 'A command execution Observable of response body.', - type: 'Observable<any>' } } - } - }, - actionsApi: { - description: 'Set of API functions to work with user defined actions.', - meta: 'property', - type: 'WidgetActionsApi', - children: { - getActionDescriptors: { - description: 'Get list of action descriptors for provided actionSourceId.', - meta: 'function', - args: [ - { - name: 'actionSourceId', - description: 'Id of widget action source', - type: 'string' + }, + actionsApi: { + description: 'Set of API functions to work with user defined actions.', + meta: 'property', + type: 'WidgetActionsApi', + children: { + getActionDescriptors: { + description: 'Get list of action descriptors for provided actionSourceId.', + meta: 'function', + args: [ + { + name: 'actionSourceId', + description: 'Id of widget action source', + type: 'string' + } + ], + return: { + description: 'The list of action descriptors', + type: 'Array<WidgetActionDescriptor>' } - ], - return: { - description: 'The list of action descriptors', - type: 'Array<WidgetActionDescriptor>' + }, + handleWidgetAction: { + description: 'Handle action produced by particular action source.', + meta: 'function', + args: [ + { + name: '$event', + description: 'DOM event object associated with action.', + type: 'Event' + }, + { + name: 'descriptor', + description: 'An action descriptor.', + type: 'WidgetActionDescriptor' + }, + { + name: 'entityId', + description: 'Current entity id provided by action source if available.', + type: entityIdHref, + optional: true + }, + { + name: 'entityName', + description: 'Current entity name provided by action source if available.', + type: 'string', + optional: true + } + ] } - }, - handleWidgetAction: { - description: 'Handle action produced by particular action source.', - meta: 'function', - args: [ - { - name: '$event', - description: 'DOM event object associated with action.', - type: 'Event' - }, - { - name: 'descriptor', - description: 'An action descriptor.', - type: 'WidgetActionDescriptor' - }, - { - name: 'entityId', - description: 'Current entity id provided by action source if available.', - type: entityIdHref, - optional: true - }, - { - name: 'entityName', - description: 'Current entity name provided by action source if available.', - type: 'string', - optional: true - } - ] } - } - }, - stateController: { - description: 'Reference to Dashboard state controller, providing API to manage current dashboard state.', - meta: 'property', - type: 'IStateController', - children: { - openState: { - description: 'Navigate to new dashboard state.', - meta: 'function', - args: [ - { - name: 'id', - description: 'An id of the target dashboard state.', + }, + stateController: { + description: 'Reference to Dashboard state controller, providing API to manage current dashboard state.', + meta: 'property', + type: 'IStateController', + children: { + openState: { + description: 'Navigate to new dashboard state.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An id of the target dashboard state.', + type: 'string' + }, + { + name: 'params', + description: 'An object with state parameters to use by the new state.', + type: 'StateParams', + optional: true + }, + { + name: 'openRightLayout', + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', + type: 'boolean', + optional: true + } + ] + }, + pushAndOpenState: { + description: 'Navigate to new dashboard state and adding intermediate states.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An array state object of the target dashboard state.', + type: 'Array StateObject', + }, + { + name: 'openRightLayout', + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', + type: 'boolean', + optional: true + } + ] + }, + updateState: { + description: 'Updates current dashboard state.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An optional id of the target dashboard state to replace current state id.', + type: 'string', + optional: true + }, + { + name: 'params', + description: 'An object with state parameters to update current state parameters.', + type: 'StateParams', + optional: true + }, + { + name: 'openRightLayout', + description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', + type: 'boolean', + optional: true + } + ] + }, + getStateId: { + description: 'Get current dashboard state id.', + meta: 'function', + return: { + description: 'current dashboard state id.', type: 'string' - }, - { - name: 'params', - description: 'An object with state parameters to use by the new state.', - type: 'StateParams', - optional: true - }, - { - name: 'openRightLayout', - description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', - type: 'boolean', - optional: true } - ] - }, - pushAndOpenState: { - description: 'Navigate to new dashboard state and adding intermediate states.', - meta: 'function', - args: [ - { - name: 'id', - description: 'An array state object of the target dashboard state.', - type: 'Array StateObject', - }, - { - name: 'openRightLayout', - description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', - type: 'boolean', - optional: true + }, + getStateParams: { + description: 'Get current dashboard state parameters.', + meta: 'function', + return: { + description: 'current dashboard state parameters.', + type: 'StateParams' } - ] - }, - updateState: { - description: 'Updates current dashboard state.', - meta: 'function', - args: [ - { - name: 'id', - description: 'An optional id of the target dashboard state to replace current state id.', - type: 'string', - optional: true - }, - { - name: 'params', - description: 'An object with state parameters to update current state parameters.', - type: 'StateParams', - optional: true - }, - { - name: 'openRightLayout', - description: 'An optional boolean argument to force open right dashboard layout if present in mobile view mode.', - type: 'boolean', - optional: true + }, + getStateParamsByStateId: { + description: 'Get state parameters for particular dashboard state identified by id.', + meta: 'function', + args: [ + { + name: 'id', + description: 'An id of the target dashboard state.', + type: 'string' + } + ], + return: { + description: 'current dashboard state parameters.', + type: 'StateParams' } - ] - }, - getStateId: { - description: 'Get current dashboard state id.', - meta: 'function', - return: { - description: 'current dashboard state id.', - type: 'string' - } - }, - getStateParams: { - description: 'Get current dashboard state parameters.', - meta: 'function', - return: { - description: 'current dashboard state parameters.', - type: 'StateParams' - } - }, - getStateParamsByStateId: { - description: 'Get state parameters for particular dashboard state identified by id.', - meta: 'function', - args: [ - { - name: 'id', - description: 'An id of the target dashboard state.', - type: 'string' - } - ], - return: { - description: 'current dashboard state parameters.', - type: 'StateParams' } } } - } - }, - ...serviceCompletions + }, + ...serviceCompletions + } } } }; + +export const widgetContextCompletions: TbEditorCompletions = widgetContextCompletionsWithSettings(); diff --git a/ui-ngx/src/app/shared/models/dynamic-form.models.ts b/ui-ngx/src/app/shared/models/dynamic-form.models.ts new file mode 100644 index 0000000000..d0ee306f89 --- /dev/null +++ b/ui-ngx/src/app/shared/models/dynamic-form.models.ts @@ -0,0 +1,816 @@ +/// +/// 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 { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; +import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models'; +import { deepClone, isDefinedAndNotNull, isEmptyStr, isString, isUndefinedOrNull } from '@core/utils'; +import { JsonFormData, JsonSchema, JsonSettingsSchema, KeyLabelItem } from '@shared/legacy/json-form.models'; +import JsonFormUtils from '@shared/legacy/json-form-utils'; +import { constantColor, Font } from '@shared/models/widget-settings.models'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +export enum FormPropertyType { + text = 'text', + number = 'number', + password = 'password', + textarea = 'textarea', + switch = 'switch', + select = 'select', + radios = 'radios', + datetime = 'datetime', + image = 'image', + javascript = 'javascript', + json = 'json', + html = 'html', + css = 'css', + markdown = 'markdown', + color = 'color', + color_settings = 'color_settings', + font = 'font', + units = 'units', + icon = 'icon', + fieldset = 'fieldset', + array = 'array', + htmlSection = 'htmlSection' +} + +export const formPropertyTypes = Object.keys(FormPropertyType) as FormPropertyType[]; + +export const formPropertyTypeTranslations = new Map( + [ + [FormPropertyType.text, 'dynamic-form.property.type-text'], + [FormPropertyType.number, 'dynamic-form.property.type-number'], + [FormPropertyType.password, 'dynamic-form.property.type-password'], + [FormPropertyType.textarea, 'dynamic-form.property.type-textarea'], + [FormPropertyType.switch, 'dynamic-form.property.type-switch'], + [FormPropertyType.select, 'dynamic-form.property.type-select'], + [FormPropertyType.radios, 'dynamic-form.property.type-radios'], + [FormPropertyType.datetime, 'dynamic-form.property.type-datetime'], + [FormPropertyType.image, 'dynamic-form.property.type-image'], + [FormPropertyType.javascript, 'dynamic-form.property.type-javascript'], + [FormPropertyType.json, 'dynamic-form.property.type-json'], + [FormPropertyType.html, 'dynamic-form.property.type-html'], + [FormPropertyType.css, 'dynamic-form.property.type-css'], + [FormPropertyType.markdown, 'dynamic-form.property.type-markdown'], + [FormPropertyType.color, 'dynamic-form.property.type-color'], + [FormPropertyType.color_settings, 'dynamic-form.property.type-color-settings'], + [FormPropertyType.font, 'dynamic-form.property.type-font'], + [FormPropertyType.units, 'dynamic-form.property.type-units'], + [FormPropertyType.icon, 'dynamic-form.property.type-icon'], + [FormPropertyType.fieldset, 'dynamic-form.property.type-fieldset'], + [FormPropertyType.array, 'dynamic-form.property.type-array'], + [FormPropertyType.htmlSection, 'dynamic-form.property.type-html-section'] + ] +); + +export const formPropertyRowClasses = + ['column', 'column-xs', 'column-lt-md', 'align-start', 'no-border', 'no-gap', 'no-padding', 'same-padding']; + +export const formPropertyFieldClasses = + ['medium-width', 'flex', 'flex-xs', 'flex-lt-md']; + +export type PropertyConditionFunction = (property: FormProperty, model: any) => boolean; + +export interface FormPropertyBase { + id: string; + name: string; + group?: string; + type: FormPropertyType; + default: any; + required?: boolean; + subLabel?: string; + divider?: boolean; + fieldSuffix?: string; + disableOnProperty?: string; + condition?: string; + conditionFunction?: PropertyConditionFunction; + disabled?: boolean; + visible?: boolean; + rowClass?: string; + fieldClass?: string; +} + +export interface FormTextareaProperty extends FormPropertyBase { + rows?: number; +} + +export interface FormNumberProperty extends FormPropertyBase { + min?: number; + max?: number; + step?: number; +} + +export interface FormFieldSetProperty extends FormPropertyBase { + properties?: FormProperty[]; +} + +export interface FormArrayProperty extends FormPropertyBase { + arrayItemName?: string; + arrayItemType?: FormPropertyType; +} + +export interface FormSelectItem { + value: any; + label: string; +} + +export interface FormSelectProperty extends FormPropertyBase { + multiple?: boolean; + allowEmptyOption?: boolean; + items?: FormSelectItem[]; + minItems?: number; + maxItems?: number; +} + +export type FormPropertyDirection = 'row' | 'column'; + +export interface FormRadiosProperty extends FormPropertyBase { + direction?: FormPropertyDirection; + items?: FormSelectItem[]; +} + +export type FormPropertyDateTimeType = 'date' | 'time' | 'datetime'; + +export interface FormDateTimeProperty extends FormPropertyBase { + allowClear?: boolean; + dateTimeType?: FormPropertyDateTimeType; +} + +export interface FormJavascriptProperty extends FormPropertyBase { + helpId?: string; +} + +export interface FormMarkdownProperty extends FormPropertyBase { + helpId?: string; +} + +export interface FormHtmlSection extends FormPropertyBase { + htmlClassList?: string[]; + htmlContent?: string; +} + +export type FormProperty = FormPropertyBase & FormTextareaProperty & FormNumberProperty & FormSelectProperty & FormRadiosProperty + & FormDateTimeProperty & FormJavascriptProperty & FormMarkdownProperty & FormFieldSetProperty & FormArrayProperty & FormHtmlSection; + +export const cleanupFormProperties = (properties: FormProperty[]): FormProperty[] => { + for (const property of properties) { + cleanupFormProperty(property); + } + return properties; +} + +export const cleanupFormProperty = (property: FormProperty): FormProperty => { + if (property.type !== FormPropertyType.number) { + delete property.min; + delete property.max; + delete property.step; + } + if (property.type !== FormPropertyType.textarea) { + delete property.rows; + } + if (property.type !== FormPropertyType.fieldset) { + delete property.properties; + } else if (property.properties?.length) { + property.properties = cleanupFormProperties(property.properties); + } + if (property.type !== FormPropertyType.array) { + delete property.arrayItemName; + delete property.arrayItemType; + } + if (property.type !== FormPropertyType.select) { + delete property.multiple; + delete property.allowEmptyOption; + delete property.minItems; + delete property.maxItems; + } + if (property.type !== FormPropertyType.radios) { + delete property.direction; + } + if (![FormPropertyType.select, FormPropertyType.radios].includes(property.type)) { + delete property.items; + } + if (property.type !== FormPropertyType.datetime) { + delete property.allowClear; + delete property.dateTimeType; + } + if (![FormPropertyType.javascript, FormPropertyType.markdown].includes(property.type)) { + delete property.helpId; + } + if (property.type !== FormPropertyType.htmlSection) { + delete property.htmlClassList; + delete property.htmlContent; + } + for (const key of Object.keys(property)) { + const val = property[key]; + if (isUndefinedOrNull(val) || isEmptyStr(val)) { + delete property[key]; + } + } + return property; +} + +export enum FormPropertyContainerType { + field = 'field', + row = 'row', + fieldset = 'fieldset', + array = 'array', + htmlSection = 'htmlSection' +} + +export interface FormPropertyContainerBase { + type: FormPropertyContainerType; + label: string; + visible: boolean; +} + +export interface FormPropertyRow extends FormPropertyContainerBase { + properties?: FormProperty[]; + switch?: FormProperty; + rowClass?: string; + propertiesRowClass?: string; +} + +export interface FormPropertyField extends FormPropertyContainerBase { + property?: FormProperty; +} + +export interface FormPropertyFieldset extends FormPropertyContainerBase { + property?: FormProperty; + properties?: FormProperty[]; +} + +export interface FormPropertyArray extends FormPropertyContainerBase { + property?: FormProperty; + arrayItemProperty?: FormProperty; +} + +export interface FormPropertyHtml extends FormPropertyContainerBase { + property?: FormProperty; + safeHtml?: SafeHtml; + htmlClass?: string; +} + +export type FormPropertyContainer = FormPropertyField & FormPropertyRow & FormPropertyFieldset & FormPropertyHtml; + +export interface FormPropertyGroup { + title?: string; + containers: FormPropertyContainer[]; + visible: boolean; +} + +export const toPropertyGroups = (properties: FormProperty[], + isArrayItem: boolean, + customTranslate: CustomTranslatePipe, + sanitizer: DomSanitizer): FormPropertyGroup[] => { + const groups: {title: string, properties: FormProperty[]}[] = []; + for (let property of properties) { + if (!property.group) { + let group = groups.length ? groups[groups.length - 1] : null; + if (group && !group.title) { + group.properties.push(property); + } else { + groups.push({ + title: null, + properties: [property] + }); + } + } else { + let propertyGroup = groups.find(g => g.title === property.group); + if (!propertyGroup) { + propertyGroup = { + title: property.group, + properties: [] + }; + groups.push(propertyGroup); + } + propertyGroup.properties.push(property); + } + } + return groups.map(g => ({ + title: g.title, + containers: toPropertyContainers(g.properties, isArrayItem, customTranslate, sanitizer), + visible: true + })); +}; + +const toPropertyContainers = (properties: FormProperty[], + isArrayItem: boolean, + customTranslate: CustomTranslatePipe, + sanitizer: DomSanitizer): FormPropertyContainer[] => { + const result: FormPropertyContainer[] = []; + for (let property of properties) { + if (property.type === FormPropertyType.array) { + const propertyArray: FormPropertyArray = { + property, + label: property.name, + type: FormPropertyContainerType.array, + visible: true + }; + const arrayItemProperty = deepClone(property); + arrayItemProperty.name = property.arrayItemName; + arrayItemProperty.type = property.arrayItemType; + arrayItemProperty.required = true; + delete arrayItemProperty.disableOnProperty; + delete arrayItemProperty.condition; + delete arrayItemProperty.conditionFunction; + delete arrayItemProperty.group; + propertyArray.arrayItemProperty = arrayItemProperty; + result.push(propertyArray); + } else if (property.type === FormPropertyType.fieldset) { + const propertyFieldset: FormPropertyFieldset = { + property, + label: property.name, + type: FormPropertyContainerType.fieldset, + properties: property.properties, + visible: true + }; + result.push(propertyFieldset); + } else if (property.type === FormPropertyType.htmlSection) { + const propertyHtml: FormPropertyHtml = { + property, + label: property.name, + type: FormPropertyContainerType.htmlSection, + htmlClass: property.htmlClassList ? property.htmlClassList.join(' ') : '', + safeHtml: sanitizer.bypassSecurityTrustHtml(property.htmlContent), + visible: true + }; + result.push(propertyHtml); + } else if (isSingleFieldPropertyType(property.type) || isArrayItem) { + const propertyField: FormPropertyField = { + property, + label: property.name, + type: FormPropertyContainerType.field, + visible: true + }; + result.push(propertyField); + } else { + let propertyRow = + result.find(r => r.type === FormPropertyContainerType.row && r.label === property.name); + if (!propertyRow) { + propertyRow = { + label: property.name, + type: FormPropertyContainerType.row, + properties: [], + rowClass: property.rowClass, + propertiesRowClass: 'row flex-end align-center', + visible: true + }; + result.push(propertyRow); + const rowClasses = (propertyRow.rowClass || '').split(' ').filter(cls => cls.trim().length > 0); + if (!rowClasses.includes('flex-wrap')) { + rowClasses.push('flex-wrap'); + } + propertyRow.rowClass = rowClasses.join(' '); + } + if (property.type === FormPropertyType.switch) { + propertyRow.switch = property; + } else { + propertyRow.properties.push(property); + } + } + } + for (let container of result.filter(c => + c.type === FormPropertyContainerType.row && !c.switch && c.properties?.length === 1)) { + const property = container.properties[0]; + if (isInputFieldPropertyType(property.type)) { + const labelText = customTranslate.transform(property.name); + if (property.type !== FormPropertyType.number && labelText.length > 40) { + container.type = FormPropertyContainerType.field; + container.property = property; + delete container.properties; + delete container.rowClass; + } else { + container.propertiesRowClass = 'gt-xs:align-center xs:flex-col gt-xs:flex-row gt-xs:justify-end'; + const rowClasses = (container.rowClass || '').split(' ').filter(cls => cls.trim().length > 0); + if (!rowClasses.includes('column-xs')) { + rowClasses.push('column-xs'); + } + if (property.fieldClass && property.fieldClass.split(' ').includes('flex')) { + container.propertiesRowClass += ' overflow-hidden'; + if (rowClasses.includes('flex-wrap')) { + rowClasses.splice(rowClasses.indexOf('flex-wrap'), 1); + } + } + container.rowClass = rowClasses.join(' '); + } + } + } + return result; +} + +export const isPropertyTypeAllowedForRow = (type: FormPropertyType): boolean => { + return !isSingleFieldPropertyType(type) && ![FormPropertyType.fieldset, FormPropertyType.array, FormPropertyType.htmlSection].includes(type); +} + +export const isSingleFieldPropertyType = (type: FormPropertyType): boolean => { + return [FormPropertyType.radios, FormPropertyType.textarea, FormPropertyType.image, FormPropertyType.javascript, FormPropertyType.json, FormPropertyType.html, + FormPropertyType.css, FormPropertyType.markdown].includes(type); +} + +export const isInputFieldPropertyType = (type: FormPropertyType): boolean => { + return [FormPropertyType.text, FormPropertyType.password, FormPropertyType.number, FormPropertyType.select, FormPropertyType.datetime, + FormPropertyType.textarea].includes(type); +} + +export const defaultFormProperties = (properties: FormProperty[]): {[id: string]: any} => { + const formProperties: {[id: string]: any} = {}; + for (const property of properties) { + if (property.type !== FormPropertyType.htmlSection) { + formProperties[property.id] = defaultFormPropertyValue(property); + } + } + return formProperties; +}; + +export const defaultFormPropertyValue = (property: FormProperty): any => { + if (property.type === FormPropertyType.array) { + return []; + } 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] = defaultFormPropertyValue(childProperty); + } + } + return propertyValue; + } else { + return property.default; + } +} + +export const propertyValid = (property: FormProperty): boolean => + !(!property.id || !property.name || !property.type || (property.type === FormPropertyType.array && !property.arrayItemType)); + +export const defaultPropertyValue = (type: FormPropertyType): any => { + switch (type) { + case FormPropertyType.text: + case FormPropertyType.textarea: + case FormPropertyType.password: + case FormPropertyType.javascript: + case FormPropertyType.json: + case FormPropertyType.html: + case FormPropertyType.css: + case FormPropertyType.markdown: + return ''; + case FormPropertyType.number: + return 0; + case FormPropertyType.switch: + return false; + case FormPropertyType.color: + return '#000'; + case FormPropertyType.color_settings: + return constantColor('#000'); + case FormPropertyType.font: + return { + size: 12, + sizeUnit: 'px', + family: 'Roboto', + weight: 'normal', + style: 'normal', + lineHeight: '1' + } as Font; + case FormPropertyType.units: + return ''; + case FormPropertyType.icon: + return 'star'; + case FormPropertyType.fieldset: + case FormPropertyType.array: + case FormPropertyType.select: + case FormPropertyType.radios: + case FormPropertyType.datetime: + case FormPropertyType.htmlSection: + case FormPropertyType.image: + return null; + } +}; + +export const formPropertyCompletions = (properties: FormProperty[], customTranslate: CustomTranslatePipe): TbEditorCompletions => { + const propertiesCompletions: TbEditorCompletions = {}; + for (const property of properties) { + if (property.type !== FormPropertyType.htmlSection) { + propertiesCompletions[property.id] = formPropertyCompletion(property, customTranslate); + } + } + return propertiesCompletions; +} + +export const formPropertyCompletion = (property: FormProperty, customTranslate: CustomTranslatePipe): TbEditorCompletion => { + let description = customTranslate.transform(property.name, property.name); + if (property.subLabel) { + description += ` ${customTranslate.transform(property.subLabel, property.subLabel)}`; + } + const isArray = property.type === FormPropertyType.array; + const type = isArray ? property.arrayItemType : property.type; + if (type === FormPropertyType.select) { + if (property.multiple || isArray) { + description += '

    Possible values of array element:'; + } else { + description += '

    Possible values:'; + } + description += `
      ${property.items.map(item => `
    • ${item.value} ${typeof item.value}
    • `).join('\n')}
    `; + } + if (type === FormPropertyType.datetime) { + if (isArray) { + description += '

    Stores array of time values in milliseconds since midnight, January 1, 1970 UTC.'; + } else { + description += '

    Stores time value in milliseconds since midnight, January 1, 1970 UTC.'; + } + } + const completion: TbEditorCompletion = { + meta: 'property', + description, + type: formPropertyCompletionType(property) + }; + if (type === FormPropertyType.fieldset && !isArray) { + completion.children = {}; + for (const childProperty of property.properties) { + if (childProperty.type !== FormPropertyType.htmlSection) { + completion.children[childProperty.id] = formPropertyCompletion(childProperty, customTranslate); + } + } + } + return completion; +}; + +const formPropertyCompletionType = (property: FormProperty): string => { + const isArray = property.type === FormPropertyType.array; + const type = isArray ? property.arrayItemType : property.type; + let typeStr: string; + switch (type) { + case FormPropertyType.text: + case FormPropertyType.password: + case FormPropertyType.textarea: + typeStr = 'string'; + break; + case FormPropertyType.number: + typeStr = 'number'; + break; + case FormPropertyType.switch: + typeStr = 'boolean'; + break; + case FormPropertyType.datetime: + typeStr = 'number'; + break; + case FormPropertyType.image: + typeStr = 'image URL string'; + break; + case FormPropertyType.select: + case FormPropertyType.radios: + const items = property.items || []; + const types: string[] = []; + items.forEach(item => { + const type = typeof item.value; + if (!types.includes(type)) { + types.push(type); + } + }); + const typesString = types.length ? types.join(' | ') : 'string'; + if (property.type === FormPropertyType.select && property.multiple) { + typeStr = `Array<${typesString}>`; + } else { + typeStr = typesString; + } + break; + case FormPropertyType.color: + typeStr = 'color string'; + break; + case FormPropertyType.color_settings: + typeStr = 'ColorProcessor'; + break; + case FormPropertyType.font: + typeStr = 'Font'; + break; + case FormPropertyType.units: + typeStr = 'units string'; + break; + case FormPropertyType.icon: + typeStr = 'icon string'; + break; + case FormPropertyType.fieldset: + typeStr = 'object'; + break; + case FormPropertyType.javascript: + typeStr = 'JavaScript function body string'; + break; + case FormPropertyType.json: + typeStr = 'JSON string'; + break; + case FormPropertyType.html: + typeStr = 'HTML string'; + break; + case FormPropertyType.css: + typeStr = 'CSS string'; + break; + case FormPropertyType.markdown: + typeStr = 'Markdown string'; + break; + default: + typeStr = 'unknown'; + break; + } + if (isArray) { + typeStr = `Array<${typeStr}>`; + } + return typeStr; +}; + + +export const jsonFormSchemaToFormProperties = (rawSchema: string | any) : FormProperty[] => { + try { + const properties: FormProperty[] = []; + let settingsSchema: JsonSettingsSchema; + if (!rawSchema || rawSchema === '') { + settingsSchema = {}; + } else { + settingsSchema = isString(rawSchema) ? JSON.parse(rawSchema) : rawSchema; + } + if (settingsSchema.schema) { + const schema = settingsSchema.schema; + const form = settingsSchema.form || ['*']; + const groupInfoes = settingsSchema.groupInfoes || []; + if (form.length > 0) { + if (groupInfoes.length) { + for (const info of groupInfoes) { + const theForm: any[] = form[info.formIndex]; + properties.push(...schemaFormToProperties(schema, theForm, info.GroupTitle)); + } + } else { + properties.push(...schemaFormToProperties(schema, form)); + } + } + } + return properties; + } catch (e) { + console.warn('Failed to convert old JSON form schema to form properties:', e); + return []; + } +} + +const schemaFormToProperties = (schema: JsonSchema, theForm: any[], groupTitle?: string): FormProperty[] => { + const merged = JsonFormUtils.merge(schema, theForm, {}, { + formDefaults: { + startEmpty: true + } + }); + return merged.map((form: JsonFormData) => jsonFormDataToProperty(form, 0, groupTitle)).filter(p => p != null); +} + +const jsonFormDataToProperty = (form: JsonFormData, level: number, groupTitle?: string): FormProperty => { + if (form.key && form.key.length > level) { + const id = form.key[level] + ''; + let property: FormProperty = { + id, + name: form.title || id, + group: groupTitle, + type: null, + default: (isDefinedAndNotNull(form.default) ? form.default : form.schema?.default) || null, + required: isDefinedAndNotNull(form.required) ? form.required : false + }; + if (form.condition?.length) { + property.condition = `return ${form.condition};`; + } + switch (form.type) { + case 'number': + property.type = FormPropertyType.number; + break; + case 'text': + property.type = FormPropertyType.text; + property.fieldClass = 'flex'; + break; + case 'password': + property.type = FormPropertyType.password; + property.fieldClass = 'flex'; + break; + case 'textarea': + property.type = FormPropertyType.textarea; + property.rows = form.rows || form.rowsMax || 2; + break; + case 'checkbox': + property.type = FormPropertyType.switch; + break; + case 'rc-select': + property.type = FormPropertyType.select; + if (form.items?.length) { + property.items = (form.items as KeyLabelItem[]).map(item => ({value: item.value, label: item.label})); + } else { + property.items = []; + } + property.multiple = form.multiple; + property.fieldClass = 'flex'; + property.allowEmptyOption = isDefinedAndNotNull(form.allowClear) ? form.allowClear : false; + if (property.multiple) { + if (typeof (form.schema as any)?.minItems === 'number') { + property.minItems = (form.schema as any).minItems; + } + if (typeof (form.schema as any)?.maxItems === 'number') { + property.maxItems = (form.schema as any).maxItems; + } + } + break; + case 'select': + property.type = FormPropertyType.select; + if (form.titleMap?.length) { + property.items = form.titleMap.map(item => ({value: item.value, label: item.name})); + } else { + property.items = []; + } + property.multiple = false; + property.fieldClass = 'flex'; + property.allowEmptyOption = false; + break; + case 'radios': + property.type = FormPropertyType.radios; + if (form.titleMap?.length) { + property.items = form.titleMap.map(item => ({value: item.value, label: item.name})); + } else { + property.items = []; + } + property.direction = form.direction === 'row' ? 'row' : 'column'; + break; + case 'date': + property.type = FormPropertyType.datetime; + property.dateTimeType = 'date'; + property.fieldClass = 'flex'; + property.allowClear = true; + break; + case 'image': + property.type = FormPropertyType.image; + break; + case 'color': + property.type = FormPropertyType.color; + break; + case 'icon': + property.type = FormPropertyType.icon; + break; + case 'fieldset': + property.type = FormPropertyType.fieldset; + property.properties = form.items ? (form.items as JsonFormData[]).map(item => + jsonFormDataToProperty(item, level+1)).filter(p => p !== null) : []; + break; + case 'javascript': + property.type = FormPropertyType.javascript; + property.helpId = form.helpId; + break; + case 'json': + property.type = FormPropertyType.json; + break; + case 'html': + property.type = FormPropertyType.html; + break; + case 'css': + property.type = FormPropertyType.css; + break; + case 'markdown': + property.type = FormPropertyType.markdown; + break; + case 'help': + property.type = FormPropertyType.htmlSection; + property.htmlContent = form.description || ''; + property.htmlClassList = form.htmlClass ? form.htmlClass.split(' ') : []; + break; + case 'array': + if (form.items?.length) { + const arrayItemSchema = form.schema.items; + if (arrayItemSchema && arrayItemSchema.type && arrayItemSchema.type !== 'array') { + if (arrayItemSchema.type === 'object') { + property.arrayItemType = FormPropertyType.fieldset; + property.arrayItemName = ''; + property.properties = form.items ? (form.items as JsonFormData[]).map(item => + jsonFormDataToProperty(item, level+2)).filter(p => p !== null) : []; + } else { + const item: JsonFormData = form.items[0] as JsonFormData; + const arrayProperty = jsonFormDataToProperty(item, 0); + arrayProperty.arrayItemType = arrayProperty.type; + arrayProperty.arrayItemName = arrayProperty.name; + arrayProperty.id = property.id; + arrayProperty.name = property.name; + arrayProperty.group = property.group; + arrayProperty.condition = property.condition; + arrayProperty.required = property.required; + property = arrayProperty; + } + property.type = FormPropertyType.array; + } + } + break; + } + if (!property.type) { + return null; + } + return property; + } + return null; +} diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index da92cc8a49..5950dcf911 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -45,6 +45,7 @@ import { HasTenantId, HasVersion } from '@shared/models/entity.models'; import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; import { TbFunction } from '@shared/models/js-function.models'; +import { FormProperty, jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export enum widgetType { @@ -154,9 +155,9 @@ export interface WidgetTypeDescriptor { templateHtml: string; templateCss: string; controllerScript: TbFunction; - settingsSchema?: string | any; - dataKeySettingsSchema?: string | any; - latestDataKeySettingsSchema?: string | any; + settingsForm?: FormProperty[]; + dataKeySettingsForm?: FormProperty[]; + latestDataKeySettingsForm?: FormProperty[]; settingsDirective?: string; dataKeySettingsDirective?: string; latestDataKeySettingsDirective?: string; @@ -194,9 +195,9 @@ export interface WidgetTypeParameters { export interface WidgetControllerDescriptor { widgetTypeFunction?: any; - settingsSchema?: string | any; - dataKeySettingsSchema?: string | any; - latestDataKeySettingsSchema?: string | any; + settingsForm?: FormProperty[]; + dataKeySettingsForm?: FormProperty[]; + latestDataKeySettingsForm?: FormProperty[]; typeParameters?: WidgetTypeParameters; actionSources?: {[actionSourceId: string]: WidgetActionSource}; } @@ -236,6 +237,30 @@ export const isValidWidgetFullFqn = (fullFqn: string): boolean => { return false; }; + +export const migrateWidgetTypeToDynamicForms = (widgetType: T): T => { + const descriptor = widgetType.descriptor; + if ((descriptor as any).settingsSchema) { + if (!descriptor.settingsForm?.length) { + descriptor.settingsForm = jsonFormSchemaToFormProperties((descriptor as any).settingsSchema); + } + delete (descriptor as any).settingsSchema; + } + if ((descriptor as any).dataKeySettingsSchema) { + if (!descriptor.dataKeySettingsForm?.length) { + descriptor.dataKeySettingsForm = jsonFormSchemaToFormProperties((descriptor as any).dataKeySettingsSchema); + } + delete (descriptor as any).dataKeySettingsSchema; + } + if ((descriptor as any).latestDataKeySettingsSchema) { + if (!descriptor.latestDataKeySettingsForm?.length) { + descriptor.latestDataKeySettingsForm = jsonFormSchemaToFormProperties((descriptor as any).latestDataKeySettingsSchema); + } + delete (descriptor as any).latestDataKeySettingsSchema; + } + return widgetType; +} + export interface WidgetType extends BaseWidgetType { descriptor: WidgetTypeDescriptor; } @@ -813,22 +838,10 @@ export interface WidgetInfo extends BaseWidgetInfo { deprecated?: boolean; } -export interface GroupInfo { - formIndex: number; - GroupTitle: string; -} - -export interface JsonSchema { - type: string; - title?: string; - properties: {[key: string]: any}; - required?: string[]; -} - -export interface JsonSettingsSchema { - schema?: JsonSchema; - form?: any[]; - groupInfoes?: GroupInfo[]; +export interface DynamicFormData { + settingsForm?: FormProperty[]; + model?: any; + settingsDirective?: string; } export interface WidgetPosition { diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index c1f243ae14..5b40dadf5f 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -121,7 +121,6 @@ import { TbJsonPipe } from '@shared/pipe/tbJson.pipe'; import { ColorPickerDialogComponent } from '@shared/components/dialog/color-picker-dialog.component'; import { ColorInputComponent } from '@shared/components/color-input.component'; import { JsFuncComponent } from '@shared/components/js-func.component'; -import { JsonFormComponent } from '@shared/components/json-form/json-form.component'; import { ConfirmDialogComponent } from '@shared/components/dialog/confirm-dialog.component'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; import { ErrorAlertDialogComponent } from '@shared/components/dialog/error-alert-dialog.component'; @@ -362,7 +361,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ColorInputComponent, MaterialIconSelectComponent, NodeScriptTestDialogComponent, - JsonFormComponent, ImageInputComponent, MultipleImageInputComponent, FileInputComponent, @@ -625,7 +623,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ColorInputComponent, MaterialIconSelectComponent, NodeScriptTestDialogComponent, - JsonFormComponent, ImageInputComponent, MultipleImageInputComponent, FileInputComponent, diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md index c0126214c7..9853bebd43 100644 --- a/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md @@ -40,32 +40,25 @@ The **Widget Editor** will be opened, pre-populated with the content of the defa {:copy-code} ``` - - Put the following JSON content inside the "Settings schema" tab of **Settings schema section**: + - Import the following JSON content inside the "Settings form" tab by clicking on 'Import form from JSON' button: ```json -{ - "schema": { - "type": "object", - "title": "AlarmTableSettings", - "properties": { - "alarmSeverityColorFunction": { - "title": "Alarm severity color function: f(severity)", - "type": "string", - "default": "if(severity == 'CRITICAL') {return 'red';} else if (severity == 'MAJOR') {return 'orange';} else return 'green'; " - } - }, - "required": [] - }, - "form": [ - { - "key": "alarmSeverityColorFunction", - "type": "javascript" - } - ] -} +[ + { + "id": "alarmSeverityColorFunction", + "name": "Alarm severity color function: f(severity)", + "type": "javascript", + "default": "if (severity == 'CRITICAL') {\n return 'red';\n} else if (severity == 'MAJOR') {\n return 'orange';\n} else return 'green';", + "required": false + } +] {:copy-code} ``` + - Clear all 'form selector' fields in the "Widget settings" tab. + + - Turn off 'Has basic mode' switch in the "Widget settings" tab. + - Put the following JavaScript code inside the "JavaScript" section: ```javascript diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md index 44abed8a46..7c7121cd44 100644 --- a/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md @@ -40,35 +40,28 @@ The **Widget Editor** will open, pre-populated with default **Control** template {:copy-code} ``` - - Put the following JSON content inside the "Settings schema" tab of **Settings schema section**: + - Import the following JSON content inside the "Settings form" tab by clicking on 'Import form from JSON' button: ```json -{ - "schema": { - "type": "object", - "title": "Settings", - "properties": { - "oneWayElseTwoWay": { - "title": "Is One Way Command", - "type": "boolean", - "default": true - }, - "requestTimeout": { - "title": "RPC request timeout", - "type": "number", - "default": 500 - } - }, - "required": [] - }, - "form": [ - "oneWayElseTwoWay", - "requestTimeout" - ] -} +[ + { + "id": "oneWayElseTwoWay", + "name": "Is One Way Command", + "type": "switch", + "default": true + }, + { + "id": "requestTimeout", + "name": "RPC request timeout", + "type": "number", + "default": 500 + } +] {:copy-code} ``` + - Clear value of 'Settings form selector' in the "Widget settings" tab. + - Put the following JavaScript code inside the "JavaScript" section: ```javascript diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md index d7c7901052..3a3bfe115d 100644 --- a/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md @@ -17,28 +17,23 @@ The **Widget Editor** will be opened pre-populated with the content of default * {:copy-code} ``` - - Put the following JSON content inside the "Settings schema" tab of **Settings schema section**: + - Import the following JSON content inside the "Settings form" tab by clicking on 'Import form from JSON' button: ```json -{ - "schema": { - "type": "object", - "title": "Settings", - "properties": { - "alertContent": { - "title": "Alert content", - "type": "string", - "default": "Content derived from alertContent property of widget settings." - } - } - }, - "form": [ - "alertContent" - ] -} +[ + { + "id": "alertContent", + "name": "Alert content", + "type": "text", + "default": "Content derived from alertContent property of widget settings.", + "fieldClass": "flex" + } +] {:copy-code} ``` + - Clear value of 'Settings form selector' in the "Widget settings" tab. + - Put the following JavaScript code inside the "JavaScript" section: ```javascript diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md index 32b005bedd..015bac28b4 100644 --- a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md @@ -10,18 +10,20 @@ Each widget function should be defined as a property of the **self** variable. In order to implement a new widget, the following JavaScript functions should be defined *(Note: each function is optional and can be implemented according to widget specific behaviour):* -|{:auto} **Function** | **Description** | -|------------------------------------|----------------------------------------------------------------------------------------| -| ``` onInit() ``` | The first function which is called when widget is ready for initialization. Should be used to prepare widget DOM, process widget settings and initial subscription information. | -| ``` onDataUpdated() ``` | Called when the new data is available from the widget subscription. Latest data can be accessed from the object of widget context (**ctx**). | -| ``` onResize() ``` | Called when widget container is resized. Latest width and height can be obtained from widget context (**ctx**). | -| ``` onEditModeChanged() ``` | Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of **ctx**. | -| ``` onMobileModeChanged() ``` | Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of **ctx**. | -| ``` onDestroy() ``` | Called when widget element is destroyed. Should be used to cleanup all resources if necessary. | -| ``` getSettingsSchema() ``` | Optional function returning widget settings schema json as alternative to **Settings schema** of settings section. | -| ``` getDataKeySettingsSchema() ``` | Optional function returning particular data key settings schema json as alternative to **Data key settings schema** tab of settings section. | -| ``` typeParameters() ``` | Returns [WidgetTypeParameters{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L151) object describing widget datasource parameters. See | | -| ``` actionSources() ``` | Returns map describing available widget action sources ([WidgetActionSource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L121)) used to define user actions. See | +| {:auto} **Function** | **Description** | +|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ``` onInit() ``` | The first function which is called when widget is ready for initialization. Should be used to prepare widget DOM, process widget settings and initial subscription information. | +| ``` onDataUpdated() ``` | Called when the new data is available from the widget subscription. Latest data can be accessed from the object of widget context (**ctx**). | +| ``` onResize() ``` | Called when widget container is resized. Latest width and height can be obtained from widget context (**ctx**). | +| ``` onEditModeChanged() ``` | Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of **ctx**. | +| ``` onMobileModeChanged() ``` | Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of **ctx**. | +| ``` onDestroy() ``` | Called when widget element is destroyed. Should be used to cleanup all resources if necessary. | +| ``` getSettingsForm() ``` | Optional function returning widget settings form array as alternative to **Settings form** tab of settings section. | +| ``` getDataKeySettingsForm() ``` | Optional function returning particular data key settings form array as alternative to **Data key settings form** tab of settings section. | +| ``` typeParameters() ``` | Returns [WidgetTypeParameters{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L151) object describing widget datasource parameters. See | | +| ``` actionSources() ``` | Returns map describing available widget action sources ([WidgetActionSource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L121)) used to define user actions. See | +| ~~getSettingsSchema()~~ | **Deprecated**. Use getSettingsForm() function. | +| ~~getDataKeySettingsSchema()~~ | **Deprecated**. Use getDataKeySettingsForm() function. |
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md index edef68210d..dbf4d991f8 100644 --- a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md +++ b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md @@ -26,7 +26,7 @@ For [Latest values{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/us label: 'Sin', // label of the dataKey. Used as display value (for ex. in the widget legend section) color: '#ffffff', // color of the key. Can be used by widget to set color of the key data (for ex. lines in line chart or segments in the pie chart). funcBody: "", // only applicable for datasource with type "function" and "function" key type. Defines body of the function to generate simulated data. - settings: {} // dataKey specific settings with structure according to the defined Data key settings json schema. See "Settings schema section". + settings: {} // dataKey specific settings with structure according to the defined Data key settings form. }, //... ] @@ -72,7 +72,7 @@ For [Alarm widget{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/use type: 'alarm', // type of the dataKey. Only "alarm" in this case. label: 'Severity', // label of the dataKey. Used as display value (for ex. as a column title in the Alarms table) color: '#ffffff', // color of the key. Can be used by widget to set color of the key data. - settings: {} // dataKey specific settings with structure according to the defined Data key settings json schema. See "Settings schema section". + settings: {} // dataKey specific settings with structure according to the defined Data key settings form. }, //... ] diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 76e1ada0e0..4eb933435f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1617,6 +1617,92 @@ "lwm2m-command": "Use the following documentation to connect the device through the LWM2M." } }, + "dynamic-form": { + "property": { + "properties": "Properties", + "property": "Property", + "id": "Id", + "name": "Name", + "type": "Type", + "type-text": "Text", + "type-password": "Password", + "type-textarea": "Text area", + "type-number": "Number", + "type-switch": "Switch", + "type-select": "Select", + "type-radios": "Radio buttons", + "type-datetime": "Date/Time", + "type-image": "Image", + "type-javascript": "JavaScript", + "type-json": "JSON", + "type-html": "HTML", + "type-css": "CSS", + "type-markdown": "Markdown", + "type-color": "Color", + "type-color-settings": "Color settings", + "type-font": "Font", + "type-units": "Units", + "type-icon": "Icon", + "type-fieldset": "Fieldset", + "type-array": "Array", + "type-html-section": "HTML section", + "group-title": "Group title", + "no-properties": "No properties configured", + "add-property": "Add property", + "property-settings": "Property settings", + "remove-property": "Remove property", + "default-value": "Default value", + "value-required": "Value required", + "number-settings": "Number settings", + "min": "Min", + "max": "Max", + "step": "Step", + "selected-options-limit": "Selected options limit", + "advanced-ui-settings": "Advanced UI settings", + "disable-on-property": "Disable on property", + "display-condition-function": "Display condition function", + "sub-label": "Sub label", + "vertical-divider-after": "Vertical divider after", + "input-field-suffix": "Input field suffix", + "property-row-classes": "Property row classes", + "property-field-classes": "Property field classes", + "not-unique-property-ids-error": "Property Ids must be unique!", + "enable-multiple-select": "Enable multiple select", + "allow-empty-select-option": "Allow empty option", + "select-options": "Select options", + "not-unique-select-option-value-error": "Select option values must be unique!", + "value": "Value", + "label": "Label", + "add-option": "Add option", + "no-options": "No options configured", + "remove-option": "Remove option", + "textarea-rows": "Textarea rows", + "help-id": "Help id", + "buttons-direction": "Buttons direction", + "direction-row": "Row", + "direction-column": "Column", + "radio-button-options": "Radio button options", + "datetime-type": "Date/Time field type", + "datetime-type-date": "Date", + "datetime-type-time": "Time", + "datetime-type-datetime": "Date/Time", + "enable-clear-button": "Enable clear button", + "html-section-settings": "HTML section settings", + "html-section-classes": "HTML section classes", + "html-section-content": "HTML section content", + "array-item": "Array item", + "item-type": "Item type", + "item-name": "Item name", + "no-items": "No items" + }, + "clear-form": "Clear form", + "clear-form-prompt": "Are you sure you want to remove all form properties?", + "import-form": "Import form from JSON", + "export-form": "Export form to JSON", + "json-file": "JSON file", + "json-content": "JSON content", + "invalid-form-json-file-error": "Unable to import form from JSON: Invalid form JSON data structure." + }, "asset-profile": { "asset-profile": "Asset profile", "asset-profiles": "Asset profiles", @@ -2501,9 +2587,7 @@ "select-entity-view": "Select entity view", "make-public": "Make entity view public", "make-private": "Make entity view private", - "start-date": "Start date", "start-ts": "Start time", - "end-date": "End date", "end-ts": "End time", "date-limits": "Date limits", "client-attributes": "Client attributes", @@ -3099,38 +3183,6 @@ "not-unique-behavior-ids-error": "Behavior Ids must be unique!", "default-settings": "Default settings" }, - "property": { - "property": "Property", - "id": "Id", - "name": "Name", - "type": "Type", - "type-text": "Text", - "type-number": "Number", - "type-switch": "Switch", - "type-color": "Color", - "type-color-settings": "Color settings", - "type-font": "Font", - "type-units": "Units", - "type-icon": "Icon", - "no-properties": "No properties configured", - "add-property": "Add property", - "property-settings": "Property settings", - "remove-property": "Remove property", - "default-value": "Default value", - "value-required": "Value required", - "number-settings": "Number settings", - "min": "Min", - "max": "Max", - "step": "Step", - "advanced-ui-settings": "Advanced UI settings", - "disable-on-property": "Disable on property", - "sub-label": "Sub label", - "vertical-divider-after": "Vertical divider after", - "input-field-suffix": "Input field suffix", - "property-row-classes": "Property row classes", - "property-field-classes": "Property field classes", - "not-unique-property-ids-error": "Property Ids must be unique!" - }, "symbol": { "symbol": "SCADA symbol", "fluid-presence": "Fluid presence", @@ -5433,9 +5485,9 @@ "html": "HTML", "tidy": "Tidy", "css": "CSS", - "settings-schema": "Settings schema", - "datakey-settings-schema": "Data key settings schema", - "latest-datakey-settings-schema": "Latest data key settings schema", + "settings-form": "Settings form", + "data-key-settings-form": "Data key settings form", + "latest-data-key-settings-form": "Latest data key settings form", "widget-settings": "Widget settings", "description": "Description", "tags": "Tags", diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 4a4d018f26..0ae2b9584b 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -54,6 +54,16 @@ padding: 12px; gap: 8px; } + &.no-padding-right { + padding-right: 0; + .mat-expansion-panel { + &.tb-settings { + > .mat-expansion-panel-header { + padding-right: 16px; + } + } + } + } &.no-padding-bottom { padding-bottom: 0; } @@ -213,7 +223,7 @@ } .mat-divider-vertical { height: 56px; - margin-top: -7px; + margin-top: -9px; margin-bottom: -7px; } .mat-mdc-form-field, tb-unit-input { diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index c394a29826..dbc69fb9dd 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -58,6 +58,10 @@ tb-root { * TYPE DEFAULTS ***************/ +* { + box-sizing: border-box; +} + body, button, html, diff --git a/ui-ngx/src/tsconfig.app.json b/ui-ngx/src/tsconfig.app.json index 7a029aa8c7..7c7b2e93d4 100644 --- a/ui-ngx/src/tsconfig.app.json +++ b/ui-ngx/src/tsconfig.app.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "../out-tsc/app", "types": ["node", "jquery", "flot", "tooltipster", "tinycolor2", "js-beautify", - "react", "react-dom", "raphael", "canvas-gauges", "systemjs"] + "raphael", "canvas-gauges", "systemjs"] }, "angularCompilerOptions": { "fullTemplateTypeCheck": true, diff --git a/ui-ngx/tsconfig.json b/ui-ngx/tsconfig.json index 61712840f4..c667775ba9 100644 --- a/ui-ngx/tsconfig.json +++ b/ui-ngx/tsconfig.json @@ -14,7 +14,6 @@ "target": "ES2022", "module": "es2020", "emitDecoratorMetadata": true, - "jsx": "react", "resolveJsonModule": true, "typeRoots": [ "node_modules/@types", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index b7e5e21e55..802ea3cf97 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -499,7 +499,7 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.24.7", "@babel/helper-module-imports@^7.25.9": +"@babel/helper-module-imports@^7.24.7", "@babel/helper-module-imports@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== @@ -1296,7 +1296,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.25.0", "@babel/runtime@^7.25.6", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.8.4": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== @@ -1382,113 +1382,6 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz#593da7a17a31a72a874e313677183334a49b01c9" integrity sha512-boghen8F0Q8D+0/Q1/1r6DUEieUJ8w2a1gIknExMSHBsJFOr2+0KUfHiVYBvucPwl3+RU5PFBK833FjFCh3BhA== -"@emotion/babel-plugin@^11.12.0": - version "11.12.0" - resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz#7b43debb250c313101b3f885eba634f1d723fcc2" - integrity sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw== - dependencies: - "@babel/helper-module-imports" "^7.16.7" - "@babel/runtime" "^7.18.3" - "@emotion/hash" "^0.9.2" - "@emotion/memoize" "^0.9.0" - "@emotion/serialize" "^1.2.0" - babel-plugin-macros "^3.1.0" - convert-source-map "^1.5.0" - escape-string-regexp "^4.0.0" - find-root "^1.1.0" - source-map "^0.5.7" - stylis "4.2.0" - -"@emotion/cache@^11.13.0", "@emotion/cache@^11.13.1": - version "11.13.1" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.13.1.tgz#fecfc54d51810beebf05bf2a161271a1a91895d7" - integrity sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw== - dependencies: - "@emotion/memoize" "^0.9.0" - "@emotion/sheet" "^1.4.0" - "@emotion/utils" "^1.4.0" - "@emotion/weak-memoize" "^0.4.0" - stylis "4.2.0" - -"@emotion/hash@^0.9.2": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" - integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== - -"@emotion/is-prop-valid@^1.3.0": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz#8d5cf1132f836d7adbe42cf0b49df7816fc88240" - integrity sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw== - dependencies: - "@emotion/memoize" "^0.9.0" - -"@emotion/memoize@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.9.0.tgz#745969d649977776b43fc7648c556aaa462b4102" - integrity sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ== - -"@emotion/react@11.13.3": - version "11.13.3" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.13.3.tgz#a69d0de2a23f5b48e0acf210416638010e4bd2e4" - integrity sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg== - dependencies: - "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.12.0" - "@emotion/cache" "^11.13.0" - "@emotion/serialize" "^1.3.1" - "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" - "@emotion/utils" "^1.4.0" - "@emotion/weak-memoize" "^0.4.0" - hoist-non-react-statics "^3.3.1" - -"@emotion/serialize@^1.2.0", "@emotion/serialize@^1.3.0", "@emotion/serialize@^1.3.1": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.3.2.tgz#e1c1a2e90708d5d85d81ccaee2dfeb3cc0cccf7a" - integrity sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA== - dependencies: - "@emotion/hash" "^0.9.2" - "@emotion/memoize" "^0.9.0" - "@emotion/unitless" "^0.10.0" - "@emotion/utils" "^1.4.1" - csstype "^3.0.2" - -"@emotion/sheet@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c" - integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg== - -"@emotion/styled@11.13.0": - version "11.13.0" - resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.13.0.tgz#633fd700db701472c7a5dbef54d6f9834e9fb190" - integrity sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA== - dependencies: - "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.12.0" - "@emotion/is-prop-valid" "^1.3.0" - "@emotion/serialize" "^1.3.0" - "@emotion/use-insertion-effect-with-fallbacks" "^1.1.0" - "@emotion/utils" "^1.4.0" - -"@emotion/unitless@^0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.10.0.tgz#2af2f7c7e5150f497bdabd848ce7b218a27cf745" - integrity sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg== - -"@emotion/use-insertion-effect-with-fallbacks@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz#1a818a0b2c481efba0cf34e5ab1e0cb2dcb9dfaf" - integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== - -"@emotion/utils@^1.4.0", "@emotion/utils@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.4.1.tgz#b3adbb43de12ee2149541c4f1337d2eb7774f0ad" - integrity sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA== - -"@emotion/weak-memoize@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" - integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== - "@es-joy/jsdoccomment@~0.49.0": version "0.49.0" resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz#e5ec1eda837c802eca67d3b29e577197f14ba1db" @@ -1676,33 +1569,6 @@ dependencies: levn "^0.4.1" -"@floating-ui/core@^1.6.0": - version "1.6.8" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.8.tgz#aa43561be075815879305965020f492cdb43da12" - integrity sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA== - dependencies: - "@floating-ui/utils" "^0.2.8" - -"@floating-ui/dom@^1.0.0": - version "1.6.11" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.11.tgz#8631857838d34ee5712339eb7cbdfb8ad34da723" - integrity sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ== - dependencies: - "@floating-ui/core" "^1.6.0" - "@floating-ui/utils" "^0.2.8" - -"@floating-ui/react-dom@^2.1.1": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31" - integrity sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A== - dependencies: - "@floating-ui/dom" "^1.0.0" - -"@floating-ui/utils@^0.2.8": - version "0.2.8" - resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" - integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== - "@flowjs/flow.js@^2.14.1": version "2.14.1" resolved "https://registry.yarnpkg.com/@flowjs/flow.js/-/flow.js-2.14.1.tgz#267d9f9d0958f32267ea5815c2a7cc09b9219304" @@ -2150,181 +2016,6 @@ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz#0aa5502d547b57abfc4ac492de68e2006e417242" integrity sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ== -"@mui/base@5.0.0-beta.58": - version "5.0.0-beta.58" - resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.58.tgz#66ae4e1aaef8cfd9ae81bd55a70ce76b02eb5d3e" - integrity sha512-P0E7ZrxOuyYqBvVv9w8k7wm+Xzx/KRu+BGgFcR2htTsGCpJNQJCSUXNUZ50MUmSU9hzqhwbQWNXhV1MBTl6F7A== - dependencies: - "@babel/runtime" "^7.25.0" - "@floating-ui/react-dom" "^2.1.1" - "@mui/types" "^7.2.15" - "@mui/utils" "6.0.0-rc.0" - "@popperjs/core" "^2.11.8" - clsx "^2.1.1" - prop-types "^15.8.1" - -"@mui/core-downloads-tracker@^6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.2.tgz#a15eb14d433100f734e56929f842c2ccc7cab691" - integrity sha512-1oE4U38/TtzLWRYWEm/m70dUbpcvBx0QvDVg6NtpOmSNQC1Mbx0X/rNvYDdZnn8DIsAiVQ+SZ3am6doSswUQ4g== - -"@mui/icons-material@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-6.1.2.tgz#3e4537c312687afbdd2fd289d5412731d5da3d11" - integrity sha512-7NNcjW5JoT9jHagrVbARA1o41vQY2xezDamtke+mEKKZmsJyejfRBOacSrPDfjZQ//lyhIjNKyzAwisxYJR47w== - dependencies: - "@babel/runtime" "^7.25.6" - -"@mui/lab@6.0.0-beta.10": - version "6.0.0-beta.10" - resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-6.0.0-beta.10.tgz#cf6dce21e8491aa00facc0d6b1cd357bfb2ed58e" - integrity sha512-eqCBz5SZS8Un9To3UcjH01AxkOOgvme/g0ZstFC8Nz1Kg5/EJMA0ByhKS5AvUMzUKrv0FXMdbuPqbBvF3bVrXg== - dependencies: - "@babel/runtime" "^7.25.6" - "@mui/base" "5.0.0-beta.58" - "@mui/system" "^6.1.1" - "@mui/types" "^7.2.17" - "@mui/utils" "^6.1.1" - clsx "^2.1.1" - prop-types "^15.8.1" - -"@mui/material@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.1.2.tgz#9f47bfa6adcf3b8245799cbf4c027e3cb949bcc6" - integrity sha512-5TtHeAVX9D5d2LYfB1GAUn29BcVETVsrQ76Dwb2SpAfQGW3JVy4deJCAd0RrIkI3eEUrsl0E4xuBdreszxdTTg== - dependencies: - "@babel/runtime" "^7.25.6" - "@mui/core-downloads-tracker" "^6.1.2" - "@mui/system" "^6.1.2" - "@mui/types" "^7.2.17" - "@mui/utils" "^6.1.2" - "@popperjs/core" "^2.11.8" - "@types/react-transition-group" "^4.4.11" - clsx "^2.1.1" - csstype "^3.1.3" - prop-types "^15.8.1" - react-is "^18.3.1" - react-transition-group "^4.4.5" - -"@mui/private-theming@^6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-6.1.2.tgz#1e093c7194dd9f8a511179e0e5c5b10798a4bfae" - integrity sha512-S8WcjZdNdi++8UhrrY8Lton5h/suRiQexvdTfdcPAlbajlvgM+kx+uJstuVIEyTb3gMkxzIZep87knZ0tqcR0g== - dependencies: - "@babel/runtime" "^7.25.6" - "@mui/utils" "^6.1.2" - prop-types "^15.8.1" - -"@mui/styled-engine@^6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.1.2.tgz#bef156ac2e47a67d49982ddb5fa4211974740a26" - integrity sha512-uKOfWkR23X39xj7th2nyTcCHqInTAXtUnqD3T5qRVdJcOPvu1rlgTleTwJC/FJvWZJBU6ieuTWDhbcx5SNViHQ== - dependencies: - "@babel/runtime" "^7.25.6" - "@emotion/cache" "^11.13.1" - "@emotion/sheet" "^1.4.0" - csstype "^3.1.3" - prop-types "^15.8.1" - -"@mui/styles@6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-6.1.2.tgz#24bc654cdb0aae369348453ee8d25e3a9c1ace56" - integrity sha512-fsQkTCyyBnjsmy7CM0LG95PJZAhTsmoC/iNk4ihVYmdubMQEeGXzeAWL8E6QBChCnANmjZwm2h5ENyLnCUUuzg== - dependencies: - "@babel/runtime" "^7.25.6" - "@emotion/hash" "^0.9.2" - "@mui/private-theming" "^6.1.2" - "@mui/types" "^7.2.17" - "@mui/utils" "^6.1.2" - clsx "^2.1.1" - csstype "^3.1.3" - hoist-non-react-statics "^3.3.2" - jss "^10.10.0" - jss-plugin-camel-case "^10.10.0" - jss-plugin-default-unit "^10.10.0" - jss-plugin-global "^10.10.0" - jss-plugin-nested "^10.10.0" - jss-plugin-props-sort "^10.10.0" - jss-plugin-rule-value-function "^10.10.0" - jss-plugin-vendor-prefixer "^10.10.0" - prop-types "^15.8.1" - -"@mui/system@6.1.2", "@mui/system@^6.1.1", "@mui/system@^6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.1.2.tgz#28840b04c6fc70780620759d67de2c20bdc7d1c7" - integrity sha512-mzW7F1ZMIYS1aLON48Nrk9c65OrVEVQ+R4lUcTWs1lCSul0VGK23eo4dmY0NX5PS7Oe4xz3P5B9tQZZ7SYgxcg== - dependencies: - "@babel/runtime" "^7.25.6" - "@mui/private-theming" "^6.1.2" - "@mui/styled-engine" "^6.1.2" - "@mui/types" "^7.2.17" - "@mui/utils" "^6.1.2" - clsx "^2.1.1" - csstype "^3.1.3" - prop-types "^15.8.1" - -"@mui/types@^7.2.15", "@mui/types@^7.2.17": - version "7.2.17" - resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.17.tgz#329826062d4079de5ea2b97007575cebbba1fdbc" - integrity sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ== - -"@mui/utils@6.0.0-rc.0": - version "6.0.0-rc.0" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.0.0-rc.0.tgz#208c12c919b5cd1731f9d14784c05c35294a893e" - integrity sha512-tBp0ILEXDL0bbDDT8PnZOjCqSm5Dfk2N0Z45uzRw+wVl6fVvloC9zw8avl+OdX1Bg3ubs/ttKn8nRNv17bpM5A== - dependencies: - "@babel/runtime" "^7.25.0" - "@mui/types" "^7.2.15" - "@types/prop-types" "^15.7.12" - clsx "^2.1.1" - prop-types "^15.8.1" - react-is "^18.3.1" - -"@mui/utils@^5.16.6": - version "5.16.6" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.16.6.tgz#905875bbc58d3dcc24531c3314a6807aba22a711" - integrity sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA== - dependencies: - "@babel/runtime" "^7.23.9" - "@mui/types" "^7.2.15" - "@types/prop-types" "^15.7.12" - clsx "^2.1.1" - prop-types "^15.8.1" - react-is "^18.3.1" - -"@mui/utils@^6.1.1", "@mui/utils@^6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.1.2.tgz#3717cd9373324a92e48c34f74385350104be652c" - integrity sha512-6+B1YZ8cCBWD1fc3RjqpclF9UA0MLUiuXhyCO+XowD/Z2ku5IlxeEhHHlgglyBWFGMu4kib4YU3CDsG5/zVjJQ== - dependencies: - "@babel/runtime" "^7.25.6" - "@mui/types" "^7.2.17" - "@types/prop-types" "^15.7.13" - clsx "^2.1.1" - prop-types "^15.8.1" - react-is "^18.3.1" - -"@mui/x-date-pickers@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.18.0.tgz#264158195aeeaf32a00519718f6c67c165b06711" - integrity sha512-12tXIoMj9vpS8fS/bS3kWPCoVrH38vNGCxgplI0vOnUrN9rJuYJz3agLPJe1S0xciTw+9W8ZSe3soaW+owoz1Q== - dependencies: - "@babel/runtime" "^7.25.6" - "@mui/utils" "^5.16.6" - "@mui/x-internals" "7.18.0" - "@types/react-transition-group" "^4.4.11" - clsx "^2.1.1" - prop-types "^15.8.1" - react-transition-group "^4.4.5" - -"@mui/x-internals@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.18.0.tgz#f079968d4f7ea93e63be9faf6ba8558d6f12923b" - integrity sha512-lzCHOWIR0cAIY1bGrWSprYerahbnH5C31ql/2OWCEjcngL2NAV1M6oKI2Vp4HheqzJ822c60UyWyapvyjSzY/A== - dependencies: - "@babel/runtime" "^7.25.6" - "@mui/utils" "^5.16.6" - "@nghedgehog/core@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@nghedgehog/core/-/core-0.0.4.tgz#4e3231847d0dac557a2e2dbdf1e3a52b106dd57c" @@ -2482,32 +2173,6 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== -"@popperjs/core@^2.11.8": - version "2.11.8" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" - integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== - -"@rc-component/portal@^1.1.0": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@rc-component/portal/-/portal-1.1.2.tgz#55db1e51d784e034442e9700536faaa6ab63fc71" - integrity sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg== - dependencies: - "@babel/runtime" "^7.18.0" - classnames "^2.3.2" - rc-util "^5.24.4" - -"@rc-component/trigger@^2.1.1": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.3.tgz#b47e945115e2d0a7f7e067dbb9ed76c91c1b4385" - integrity sha512-X1oFIpKoXAMXNDYCviOmTfuNuYxE4h5laBsyCqVAVMjNHxoF3/uiyA7XdegK1XbCvBbCZ6P6byWrEoDRpKL8+A== - dependencies: - "@babel/runtime" "^7.23.2" - "@rc-component/portal" "^1.1.0" - classnames "^2.3.2" - rc-motion "^2.0.0" - rc-resize-observer "^1.3.1" - rc-util "^5.38.0" - "@rollup/rollup-android-arm-eabi@4.22.4": version "4.22.4" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz#8b613b9725e8f9479d142970b106b6ae878610d5" @@ -3118,16 +2783,6 @@ dependencies: undici-types "~6.19.2" -"@types/parse-json@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" - integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== - -"@types/prop-types@*", "@types/prop-types@^15.7.12", "@types/prop-types@^15.7.13": - version "15.7.13" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" - integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== - "@types/qs@*": version "6.9.16" resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.16.tgz#52bba125a07c0482d26747d5d4947a64daf8f794" @@ -3143,28 +2798,6 @@ resolved "https://registry.yarnpkg.com/@types/raphael/-/raphael-2.3.9.tgz#d53bb8930431524f42987a8a19815c0d42a61eb5" integrity sha512-K1dZwoLNvEN+mvleFU/t2swG9Z4SE5Vub7dA5wDYojH0bVTQ8ZAP+lNsl91t1njdu/B+roSEL4QXC67I7Hpiag== -"@types/react-dom@18.3.0": - version "18.3.0" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" - integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== - dependencies: - "@types/react" "*" - -"@types/react-transition-group@^4.4.11": - version "4.4.11" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" - integrity sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA== - dependencies: - "@types/react" "*" - -"@types/react@*", "@types/react@18.3.10": - version "18.3.10" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.10.tgz#6edc26dc22ff8c9c226d3c7bf8357b013c842219" - integrity sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - "@types/retry@0.12.2": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" @@ -3470,7 +3103,7 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -ace-builds@1.36.5, ace-builds@^1.32.8: +ace-builds@1.36.5: version "1.36.5" resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.36.5.tgz#ae9cc7a32eccc2f484926131c00545cd6b78a6a6" integrity sha512-mZ5KVanRT6nLRDLqtG/1YQQLX/gZVC/v526cm1Ru/MTSlrbweSmqv2ZT0d2GaHpJq035MwCMIrj+LgDAUnDXrg== @@ -3790,11 +3423,6 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -attr-accept@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" - integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== - autoprefixer@10.4.20, autoprefixer@^10.4.20: version "10.4.20" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" @@ -3827,15 +3455,6 @@ babel-loader@9.1.3: find-cache-dir "^4.0.0" schema-utils "^4.0.0" -babel-plugin-macros@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" - integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== - dependencies: - "@babel/runtime" "^7.12.5" - cosmiconfig "^7.0.0" - resolve "^1.19.0" - babel-plugin-polyfill-corejs2@^0.4.10: version "0.4.11" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" @@ -4129,11 +3748,6 @@ ci-info@^3.7.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -classnames@2.x, classnames@^2.2.1, classnames@^2.2.6, classnames@^2.3.2: - version "2.5.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" - integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== - clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -4212,11 +3826,6 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -clsx@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -4363,7 +3972,7 @@ content-type@~1.0.4, content-type@~1.0.5: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== -convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.7.0: +convert-source-map@^1.5.1, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== @@ -4433,17 +4042,6 @@ cose-base@^2.2.0: dependencies: layout-base "^2.0.0" -cosmiconfig@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" - integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - cosmiconfig@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" @@ -4524,14 +4122,6 @@ css-select@^5.1.0: domutils "^3.0.1" nth-check "^2.0.1" -css-vendor@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" - integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== - dependencies: - "@babel/runtime" "^7.8.3" - is-in-browser "^1.0.2" - css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" @@ -4542,11 +4132,6 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -csstype@^3.0.2, csstype@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" - integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== - cytoscape-cose-bilkent@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b" @@ -5062,14 +4647,6 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -dom-helpers@^5.0.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" - integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== - dependencies: - "@babel/runtime" "^7.8.7" - csstype "^3.0.2" - dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" @@ -5703,13 +5280,6 @@ file-entry-cache@^8.0.0: dependencies: flat-cache "^4.0.0" -file-selector@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" - integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== - dependencies: - tslib "^2.4.0" - fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -5745,11 +5315,6 @@ find-replace@^3.0.0: dependencies: array-back "^3.0.1" -find-root@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" - integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== - find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -6126,13 +5691,6 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: dependencies: function-bind "^1.1.2" -hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - hosted-git-info@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz#9b751acac097757667f30114607ef7b661ff4f17" @@ -6267,11 +5825,6 @@ hyperdyperid@^1.2.0: resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== -hyphenate-style-name@^1.0.3: - version "1.1.0" - resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz#1797bf50369588b47b72ca6d5e65374607cf4436" - integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw== - iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -6511,11 +6064,6 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-in-browser@^1.0.2, is-in-browser@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" - integrity sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g== - is-inside-container@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" @@ -6746,7 +6294,7 @@ js-cookie@^3.0.5: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -6861,76 +6409,6 @@ jsonparse@^1.3.1: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -jss-plugin-camel-case@^10.10.0: - version "10.10.0" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz#27ea159bab67eb4837fa0260204eb7925d4daa1c" - integrity sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw== - dependencies: - "@babel/runtime" "^7.3.1" - hyphenate-style-name "^1.0.3" - jss "10.10.0" - -jss-plugin-default-unit@^10.10.0: - version "10.10.0" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz#db3925cf6a07f8e1dd459549d9c8aadff9804293" - integrity sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.10.0" - -jss-plugin-global@^10.10.0: - version "10.10.0" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz#1c55d3c35821fab67a538a38918292fc9c567efd" - integrity sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.10.0" - -jss-plugin-nested@^10.10.0: - version "10.10.0" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz#db872ed8925688806e77f1fc87f6e62264513219" - integrity sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.10.0" - tiny-warning "^1.0.2" - -jss-plugin-props-sort@^10.10.0: - version "10.10.0" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz#67f4dd4c70830c126f4ec49b4b37ccddb680a5d7" - integrity sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.10.0" - -jss-plugin-rule-value-function@^10.10.0: - version "10.10.0" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz#7d99e3229e78a3712f78ba50ab342e881d26a24b" - integrity sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g== - dependencies: - "@babel/runtime" "^7.3.1" - jss "10.10.0" - tiny-warning "^1.0.2" - -jss-plugin-vendor-prefixer@^10.10.0: - version "10.10.0" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz#c01428ef5a89f2b128ec0af87a314d0c767931c7" - integrity sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg== - dependencies: - "@babel/runtime" "^7.3.1" - css-vendor "^2.0.8" - jss "10.10.0" - -jss@10.10.0, jss@^10.10.0: - version "10.10.0" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.10.0.tgz#a75cc85b0108c7ac8c7b7d296c520a3e4fbc6ccc" - integrity sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw== - dependencies: - "@babel/runtime" "^7.3.1" - csstype "^3.0.2" - is-in-browser "^1.1.3" - tiny-warning "^1.0.2" - jstree-bootstrap-theme@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/jstree-bootstrap-theme/-/jstree-bootstrap-theme-1.0.1.tgz#7d5edc73a846e8da7f94f57a1cc5ddee9d9eab4b" @@ -7221,22 +6699,12 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== - -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@4.17.21, lodash@^4.0.1, lodash@^4.17.14: +lodash@4.17.21, lodash@^4.17.14: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -7265,13 +6733,6 @@ log-update@^6.1.0: strip-ansi "^7.1.0" wrap-ansi "^9.0.0" -loose-envify@^1.1.0, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - lru-cache@^10.0.1, lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" @@ -7913,7 +7374,7 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -object-assign@^4.0.1, object-assign@^4.1.1: +object-assign@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -8185,7 +7646,7 @@ parse-imports@^2.1.1: es-module-lexer "^1.5.3" slashes "^3.0.12" -parse-json@^5.0.0, parse-json@^5.2.0: +parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -8292,11 +7753,6 @@ path-to-regexp@0.1.10: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - path-type@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" @@ -8505,11 +7961,6 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.8.3: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== - prismjs@^1.27.0, prismjs@^1.28.0: version "1.29.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" @@ -8538,15 +7989,6 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -prop-types@^15.6.2, prop-types@^15.8.1: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -8632,128 +8074,6 @@ rbush@^3.0.1: dependencies: quickselect "^2.0.0" -rc-motion@^2.0.0, rc-motion@^2.0.1: - version "2.9.3" - resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.9.3.tgz#b1bdaf816f1ccb3e4b3b0c531c3037a59286379e" - integrity sha512-rkW47ABVkic7WEB0EKJqzySpvDqwl60/tdkY7hWP7dYnh5pm0SzJpo54oW3TDUGXV5wfxXFmMkxrzRRbotQ0+w== - dependencies: - "@babel/runtime" "^7.11.1" - classnames "^2.2.1" - rc-util "^5.43.0" - -rc-overflow@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/rc-overflow/-/rc-overflow-1.3.2.tgz#72ee49e85a1308d8d4e3bd53285dc1f3e0bcce2c" - integrity sha512-nsUm78jkYAoPygDAcGZeC2VwIg/IBGSodtOY3pMof4W3M9qRJgqaDYm03ZayHlde3I6ipliAxbN0RUcGf5KOzw== - dependencies: - "@babel/runtime" "^7.11.1" - classnames "^2.2.1" - rc-resize-observer "^1.0.0" - rc-util "^5.37.0" - -rc-resize-observer@^1.0.0, rc-resize-observer@^1.3.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.4.0.tgz#7bba61e6b3c604834980647cce6451914750d0cc" - integrity sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q== - dependencies: - "@babel/runtime" "^7.20.7" - classnames "^2.2.1" - rc-util "^5.38.0" - resize-observer-polyfill "^1.5.1" - -rc-select@14.15.2: - version "14.15.2" - resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.15.2.tgz#d85fcf3a708bdf837b003feeed653347b8980ad0" - integrity sha512-oNoXlaFmpqXYcQDzcPVLrEqS2J9c+/+oJuGrlXeVVX/gVgrbHa5YcyiRUXRydFjyuA7GP3elRuLF7Y3Tfwltlw== - dependencies: - "@babel/runtime" "^7.10.1" - "@rc-component/trigger" "^2.1.1" - classnames "2.x" - rc-motion "^2.0.1" - rc-overflow "^1.3.1" - rc-util "^5.16.1" - rc-virtual-list "^3.5.2" - -rc-util@^5.15.0, rc-util@^5.16.1, rc-util@^5.24.4, rc-util@^5.37.0, rc-util@^5.38.0, rc-util@^5.43.0: - version "5.43.0" - resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.43.0.tgz#bba91fbef2c3e30ea2c236893746f3e9b05ecc4c" - integrity sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw== - dependencies: - "@babel/runtime" "^7.18.3" - react-is "^18.2.0" - -rc-virtual-list@3.5.2, rc-virtual-list@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.5.2.tgz#5e1028869bae900eacbae6788d4eca7210736006" - integrity sha512-sE2G9hTPjVmatQni8OP2Kx33+Oth6DMKm67OblBBmgMBJDJQOOFpSGH7KZ6Pm85rrI2IGxDRXZCr0QhYOH2pfQ== - dependencies: - "@babel/runtime" "^7.20.0" - classnames "^2.2.6" - rc-resize-observer "^1.0.0" - rc-util "^5.15.0" - -react-ace@12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-12.0.0.tgz#d40afc7382092109eead7227d9426f55dcc2209d" - integrity sha512-PstU6CSMfYIJknb4su2Fa0WgLXzq2ufQgR6fjcSWuGT1hGTHkBzuKw+SncV8PuLCdSJBJc1VehPhyeXlWByG/g== - dependencies: - ace-builds "^1.32.8" - diff-match-patch "^1.0.5" - lodash.get "^4.4.2" - lodash.isequal "^4.5.0" - prop-types "^15.8.1" - -react-dom@18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" - -react-dropzone@14.2.9: - version "14.2.9" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.9.tgz#193a33f9035e29fc91abf24e50de5d66cfa7c8c0" - integrity sha512-jRZsMC7h48WONsOLHcmhyn3cRWJoIPQjPApvt/sJVfnYaB3Qltn025AoRTTJaj4WdmmgmLl6tUQg1s0wOhpodQ== - dependencies: - attr-accept "^2.2.2" - file-selector "^0.6.0" - prop-types "^15.8.1" - -react-is@^16.13.1, react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-is@^18.2.0, react-is@^18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" - integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== - -react-transition-group@^4.4.5: - version "4.4.5" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" - integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== - dependencies: - "@babel/runtime" "^7.5.5" - dom-helpers "^5.0.1" - loose-envify "^1.4.0" - prop-types "^15.6.2" - -react@18.3.1: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== - dependencies: - loose-envify "^1.1.0" - -reactcss@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" - integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A== - dependencies: - lodash "^4.0.1" - read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" @@ -8888,11 +8208,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -resize-observer-polyfill@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" - integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -8909,7 +8224,7 @@ resolve-url-loader@5.0.0: postcss "^8.2.14" source-map "0.6.1" -resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.4, resolve@^1.22.8: +resolve@1.22.8, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.22.4, resolve@^1.22.8: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -9092,13 +8407,6 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" - schema-inspector@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/schema-inspector/-/schema-inspector-2.1.0.tgz#85096fbc78162a420262ed41b82e60ac927767b2" @@ -9408,11 +8716,6 @@ source-map@0.7.4: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== -source-map@^0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== - spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" @@ -9621,11 +8924,6 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -stylis@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" - integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== - stylis@^4.3.1: version "4.3.4" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.4.tgz#ca5c6c4a35c4784e4e93a2a24dc4e9fa075250a4" @@ -9810,11 +9108,6 @@ tiny-emitter@^2.0.0: resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== -tiny-warning@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - tinycolor2@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" @@ -9941,11 +9234,6 @@ tuf-js@^2.2.1: debug "^4.3.4" make-fetch-happen "^13.0.1" -tv4@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/tv4/-/tv4-1.3.0.tgz#d020c846fadd50c855abb25ebaecc68fc10f7963" - integrity sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -10504,11 +9792,6 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - yaml@^2.2.2, yaml@^2.3.4: version "2.6.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773"