Browse Source

UI: Implement action button widget.

pull/10132/head
Igor Kulikov 2 years ago
parent
commit
4b29a8f3ff
  1. 13
      application/src/main/data/json/system/widget_bundles/buttons.json
  2. 27
      application/src/main/data/json/system/widget_types/action_button.json
  3. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  4. 26
      ui-ngx/src/app/core/services/utils.service.ts
  5. 16
      ui-ngx/src/app/modules/common/modules-map.ts
  6. 1
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  7. 3
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  8. 11
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  9. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  10. 77
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html
  11. 139
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.ts
  12. 18
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html
  13. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  14. 30
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  15. 5
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  16. 17
      ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
  17. 14
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  18. 46
      ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.scss
  19. 35
      ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.html
  20. 28
      ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.scss
  21. 100
      ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.ts
  22. 67
      ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.models.ts
  23. 8
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html
  24. 30
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.scss
  25. 26
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts
  26. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/action-settings-button.component.html
  27. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/action-settings-button.scss
  28. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/action-settings-panel.component.scss
  29. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html
  30. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss
  31. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts
  32. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html
  33. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.scss
  34. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts
  35. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action.models.ts
  36. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-sample-css.raw
  37. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-sample-html.raw
  38. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-sample-js.raw
  39. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html
  40. 21
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts
  41. 29
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts
  42. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html
  43. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts
  44. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts
  45. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html
  46. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts
  47. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts
  48. 42
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html
  49. 88
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts
  50. 136
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts
  51. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html
  52. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts
  53. 97
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html
  54. 141
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.ts
  55. 93
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.html
  56. 68
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.scss
  57. 171
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.ts
  58. 44
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html
  59. 46
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.scss
  60. 157
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts
  61. 47
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  62. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html
  63. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts
  64. 3
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  65. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  66. 2
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  67. 7
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  68. 24
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  69. 1
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
  70. 16
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  71. 2
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  72. 39
      ui-ngx/src/app/shared/components/button/widget-button.component.html
  73. 189
      ui-ngx/src/app/shared/components/button/widget-button.component.scss
  74. 178
      ui-ngx/src/app/shared/components/button/widget-button.component.ts
  75. 259
      ui-ngx/src/app/shared/components/button/widget-button.models.ts
  76. 19
      ui-ngx/src/app/shared/models/action-widget-settings.models.ts
  77. 27
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  78. 26
      ui-ngx/src/app/shared/models/widget.models.ts
  79. 7
      ui-ngx/src/app/shared/shared.module.ts
  80. 47
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  81. 17
      ui-ngx/src/assets/widget/button/basic.svg
  82. 19
      ui-ngx/src/assets/widget/button/filled.svg
  83. 20
      ui-ngx/src/assets/widget/button/outlined.svg
  84. 23
      ui-ngx/src/assets/widget/button/underlined.svg
  85. 8
      ui-ngx/src/form.scss

13
application/src/main/data/json/system/widget_bundles/buttons.json

@ -0,0 +1,13 @@
{
"widgetsBundle": {
"alias": "buttons",
"title": "Buttons",
"image": null,
"description": null,
"order": 7500,
"name": "Buttons"
},
"widgetTypeFqns": [
"action_button"
]
}

27
application/src/main/data/json/system/widget_types/action_button.json

File diff suppressed because one or more lines are too long

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -91,6 +91,7 @@ export interface WidgetActionsApi {
entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void;
elementClick: ($event: Event) => void;
cardClick: ($event: Event) => void;
click: ($event: Event) => void;
getActiveEntityInfo: () => SubscriptionEntityInfo;
openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void;

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

@ -17,7 +17,7 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../src/typings/rawloader.typings.d.ts" />
import { Inject, Injectable, NgZone } from '@angular/core';
import { Inject, Injectable, NgZone, Renderer2 } from '@angular/core';
import { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models';
import {
@ -55,8 +55,9 @@ import {
TelemetryType
} from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import { DatePipe } from '@angular/common';
import { DatePipe, DOCUMENT } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
import cssjs from '@core/css/css';
const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g');
@ -116,6 +117,7 @@ export class UtilsService {
defaultAlarmDataKeys: Array<DataKey> = [];
constructor(@Inject(WINDOW) private window: Window,
@Inject(DOCUMENT) private document: Document,
private zone: NgZone,
private datePipe: DatePipe,
private translate: TranslateService) {
@ -502,4 +504,24 @@ export class UtilsService {
return base64toObj(b64Encoded);
}
public applyCssToElement(renderer: Renderer2, element: any, cssClassPrefix: string, css: string): string {
const cssParser = new cssjs();
cssParser.testMode = false;
const cssClass = `${cssClassPrefix}-${guid()}`;
cssParser.cssPreviewNamespace = cssClass;
cssParser.createStyleElement(cssClass, css);
renderer.addClass(element, cssClass);
return cssClass;
}
public clearCssElement(renderer: Renderer2, cssClass: string, element?: any): void {
if (element) {
renderer.removeClass(element, cssClass);
}
const el = this.document.getElementById(cssClass);
if (el) {
el.parentNode.removeChild(el);
}
}
}

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

@ -14,6 +14,8 @@
/// limitations under the License.
///
/* eslint-disable max-len */
import * as AngularAnimations from '@angular/animations';
import * as AngularCore from '@angular/core';
import * as AngularCommon from '@angular/common';
@ -226,9 +228,9 @@ import * as DataKeyConfigComponent from '@home/components/widget/config/data-key
import * as LegendConfigComponent from '@home/components/widget/lib/settings/common/legend-config.component';
import * as ManageWidgetActionsComponent from '@home/components/widget/action/manage-widget-actions.component';
import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.component';
import * as CustomActionPrettyResourcesTabsComponent from '@home/components/widget/config/action/custom-action-pretty-resources-tabs.component';
import * as CustomActionPrettyEditorComponent from '@home/components/widget/config/action/custom-action-pretty-editor.component';
import * as MobileActionEditorComponent from '@home/components/widget/config/action/mobile-action-editor.component';
import * as CustomActionPrettyResourcesTabsComponent from '@home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component';
import * as CustomActionPrettyEditorComponent from '@home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component';
import * as MobileActionEditorComponent from '@home/components/widget/lib/settings/common/action/mobile-action-editor.component';
import * as CustomDialogService from '@home/components/widget/dialog/custom-dialog.service';
import * as CustomDialogContainerComponent from '@home/components/widget/dialog/custom-dialog-container.component';
import * as ImportDialogComponent from '@shared/import-export/import-dialog.component';
@ -261,7 +263,6 @@ import * as FilterPredicateValueComponent from '@home/components/filter/filter-p
import * as TenantProfileComponent from '@home/components/profile/tenant-profile.component';
import * as TenantProfileDialogComponent from '@home/components/profile/tenant-profile-dialog.component';
import * as TenantProfileDataComponent from '@home/components/profile/tenant-profile-data.component';
// eslint-disable-next-line max-len
import * as DefaultDeviceProfileConfigurationComponent from '@home/components/profile/device/default-device-profile-configuration.component';
import * as DeviceProfileConfigurationComponent from '@home/components/profile/device/device-profile-configuration.component';
import * as DeviceProfileComponent from '@home/components/profile/device-profile.component';
@ -286,7 +287,6 @@ import * as AlarmScheduleInfoComponent from '@home/components/profile/alarm/alar
import * as AlarmScheduleDialogComponent from '@home/components/profile/alarm/alarm-schedule-dialog.component';
import * as EditAlarmDetailsDialogComponent from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
import * as AlarmRuleConditionDialogComponent from '@home/components/profile/alarm/alarm-rule-condition-dialog.component';
// eslint-disable-next-line max-len
import * as DefaultTenantProfileConfigurationComponent from '@home/components/profile/tenant/default-tenant-profile-configuration.component';
import * as TenantProfileConfigurationComponent from '@home/components/profile/tenant/tenant-profile-configuration.component';
import * as SmsProviderConfigurationComponent from '@home/components/sms/sms-provider-configuration.component';
@ -541,9 +541,9 @@ class ModulesMap implements IModulesMap {
'@home/components/widget/lib/settings/common/legend-config.component': LegendConfigComponent,
'@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent,
'@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent,
'@home/components/widget/config/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent,
'@home/components/widget/config/action/custom-action-pretty-editor.component': CustomActionPrettyEditorComponent,
'@home/components/widget/config/action/mobile-action-editor.component': MobileActionEditorComponent,
'@home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent,
'@home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component': CustomActionPrettyEditorComponent,
'@home/components/widget/lib/settings/common/action/mobile-action-editor.component': MobileActionEditorComponent,
'@home/components/widget/dialog/custom-dialog.service': CustomDialogService,
'@home/components/widget/dialog/custom-dialog-container.component': CustomDialogContainerComponent,
'@home/components/attribute/add-widget-to-dashboard-dialog.component': AddWidgetToDashboardDialogComponent,

1
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -71,6 +71,7 @@
[dashboardStyle]="dashboardStyle"
[backgroundImage]="backgroundImage"
[isEdit]="isEdit"
[isPreview]="isPreview"
[isMobile]="isMobileSize"
[isEditActionEnabled]="isEditActionEnabled"
[isExportActionEnabled]="isExportActionEnabled"

3
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts

@ -97,6 +97,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@Input()
isEdit: boolean;
@Input()
isPreview: boolean;
@Input()
autofillHeight: boolean;

11
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts

@ -38,13 +38,12 @@ import {
} from '@home/components/widget/action/manage-widget-actions.component.models';
import { UtilsService } from '@core/services/utils.service';
import {
actionDescriptorToAction,
actionDescriptorToAction, defaultWidgetAction,
WidgetActionSource,
WidgetActionType,
widgetType
} from '@shared/models/widget.models';
import { takeUntil } from 'rxjs/operators';
import { CustomActionEditorCompleter } from '@home/components/widget/config/action/custom-action.models';
import { CustomActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models';
import { WidgetService } from '@core/http/widget.service';
export interface WidgetActionDialogData {
@ -92,11 +91,7 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
id: this.utils.guid(),
name: '',
icon: 'more_horiz',
type: WidgetActionType.updateDashboardState,
targetDashboardStateId: null,
openRightLayout: false,
setEntityId: data.widgetType !== widgetType.static,
stateEntityParamName: null
...defaultWidgetAction(data.widgetType !== widgetType.static)
};
} else {
this.action = this.data.action;

12
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -94,6 +94,9 @@ import {
import {
SingleSwitchBasicConfigComponent
} from '@home/components/widget/config/basic/rpc/single-switch-basic-config.component';
import {
ActionButtonBasicConfigComponent
} from '@home/components/widget/config/basic/button/action-button-basic-config.component';
@NgModule({
declarations: [
@ -123,7 +126,8 @@ import {
DoughnutBasicConfigComponent,
RangeChartBasicConfigComponent,
BarChartWithLabelsBasicConfigComponent,
SingleSwitchBasicConfigComponent
SingleSwitchBasicConfigComponent,
ActionButtonBasicConfigComponent
],
imports: [
CommonModule,
@ -157,7 +161,8 @@ import {
DoughnutBasicConfigComponent,
RangeChartBasicConfigComponent,
BarChartWithLabelsBasicConfigComponent,
SingleSwitchBasicConfigComponent
SingleSwitchBasicConfigComponent,
ActionButtonBasicConfigComponent
]
})
export class BasicWidgetConfigModule {
@ -185,5 +190,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-doughnut-basic-config': DoughnutBasicConfigComponent,
'tb-range-chart-basic-config': RangeChartBasicConfigComponent,
'tb-bar-chart-with-labels-basic-config': BarChartWithLabelsBasicConfigComponent,
'tb-single-switch-basic-config': SingleSwitchBasicConfigComponent
'tb-single-switch-basic-config': SingleSwitchBasicConfigComponent,
'tb-action-button-basic-config': ActionButtonBasicConfigComponent
};

77
ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html

@ -0,0 +1,77 @@
<!--
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.
-->
<ng-container [formGroup]="actionButtonWidgetConfigForm">
<tb-datasources
*ngIf="!widgetEditMode"
[configMode]="basicMode"
hideDatasourceLabel
hideDataKeys
forceSingleDatasource
formControlName="datasources">
</tb-datasources>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.action-button.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.action-button.on-click-hint' | translate}}" translate>widgets.action-button.on-click</div>
<tb-widget-action-settings fxFlex
panelTitle="widgets.action-button.on-click"
[callbacks]="callbacks"
[widgetType]="widgetType"
formControlName="onClickAction">
</tb-widget-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.button-state.activated-state-hint' | translate}}" translate>widgets.button-state.activated-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.button-state.activated-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.button-state.activated"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="activatedState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.button-state.disabled-state-hint' | translate}}" translate>widgets.button-state.disabled-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.button-state.disabled-state"
[valueType]="valueType.BOOLEAN"
stateLabel="widgets.button-state.disabled"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="disabledState"></tb-get-value-action-settings>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-widget-button-appearance
[borderRadius]="actionButtonWidgetConfigForm.get('borderRadius').value"
formControlName="appearance">
</tb-widget-button-appearance>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.card-appearance</div>
<div class="tb-form-row space-between">
<div>{{ 'widget-config.card-border-radius' | translate }}</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="borderRadius" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
</ng-container>

139
ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.ts

@ -0,0 +1,139 @@
///
/// 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 } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
actionDescriptorToAction,
Datasource,
defaultWidgetAction,
TargetDevice,
WidgetAction,
WidgetConfig,
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { guid } from '@core/utils';
import { ValueType } from '@shared/models/constants';
import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models';
import {
actionButtonDefaultSettings,
ActionButtonWidgetSettings
} from '@home/components/widget/lib/button/action-button-widget.models';
@Component({
selector: 'tb-action-button-basic-config',
templateUrl: './action-button-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class ActionButtonBasicConfigComponent extends BasicWidgetConfigComponent {
get targetDevice(): TargetDevice {
const datasources: Datasource[] = this.actionButtonWidgetConfigForm.get('datasources').value;
return getTargetDeviceFromDatasources(datasources);
}
valueType = ValueType;
actionButtonWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.actionButtonWidgetConfigForm;
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: ActionButtonWidgetSettings = {...actionButtonDefaultSettings, ...(configData.config.settings || {})};
const onClickAction = this.getOnClickAction(configData.config);
this.actionButtonWidgetConfigForm = this.fb.group({
datasources: [configData.config.datasources, []],
onClickAction: [onClickAction, []],
activatedState: [settings.activatedState, []],
disabledState: [settings.disabledState, []],
appearance: [settings.appearance, []],
borderRadius: [configData.config.borderRadius, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.datasources = config.datasources;
this.setOnClickAction(this.widgetConfig.config, config.onClickAction);
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.activatedState = config.activatedState;
this.widgetConfig.config.settings.disabledState = config.disabledState;
this.widgetConfig.config.settings.appearance = config.appearance;
this.widgetConfig.config.borderRadius = config.borderRadius;
return this.widgetConfig;
}
private getOnClickAction(config: WidgetConfig): WidgetAction {
let clickAction: WidgetAction;
const actions = config.actions;
if (actions && actions.click) {
const descriptors = actions.click;
if (descriptors?.length) {
const descriptor = descriptors[0];
clickAction = actionDescriptorToAction(descriptor);
}
}
if (!clickAction) {
clickAction = defaultWidgetAction();
}
return clickAction;
}
private setOnClickAction(config: WidgetConfig, clickAction: WidgetAction): void {
let actions = config.actions;
if (!actions) {
actions = {};
config.actions = actions;
}
let descriptors = actions.click;
if (!descriptors) {
descriptors = [];
actions.click = descriptors;
}
let descriptor = descriptors[0];
if (!descriptor) {
descriptor = {
id: guid(),
name: 'onClick',
icon: 'more_horiz',
...clickAction
};
descriptors[0] = descriptor;
} else {
descriptors[0] = {...descriptor, ...clickAction};
}
}
}

18
ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html

@ -20,30 +20,36 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.single-switch.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.initial-state-hint' | translate}}" translate>widgets.value-action.initial-state</div>
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.value-action.initial-state"
panelTitle="widgets.rpc-state.initial-state"
[valueType]="valueType.BOOLEAN"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
stateLabel="widgets.rpc-state.on"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-on-hint' | translate}}" translate>widgets.value-action.turn-on</div>
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-on-hint' | translate}}" translate>widgets.rpc-state.turn-on</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.value-action.turn-on"
panelTitle="widgets.rpc-state.turn-on"
[valueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="onUpdateState"></tb-set-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-off-hint' | translate}}" translate>widgets.value-action.turn-off</div>
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-off-hint' | translate}}" translate>widgets.rpc-state.turn-off</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.value-action.turn-off"
panelTitle="widgets.rpc-state.turn-off"
[valueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="offUpdateState"></tb-set-value-action-settings>
</div>
</div>

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

@ -42,14 +42,14 @@
style="height: 56px; margin-bottom: 22px;"
formControlName="alarmFilterConfig"></tb-alarm-filter-config>
<tb-entity-autocomplete *ngIf="datasourceFormGroup.get('type').value === datasourceType.device"
required
[required]="!datasourcesOptional"
[entityType]="entityType.DEVICE"
formControlName="deviceId">
</tb-entity-autocomplete>
<tb-entity-alias-select
*ngIf="datasourceFormGroup.get('type').value !== datasourceType.device && datasourceFormGroup.get('type').value !== datasourceType.alarmCount"
[showLabel]="true"
[tbRequired]="true"
[tbRequired]="!datasourcesOptional"
[aliasController]="aliasController"
formControlName="entityAliasId"
[callbacks]="entityAliasSelectCallbacks">

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

@ -95,6 +95,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.widgetConfigComponent.modelValue?.typeParameters?.dataKeysOptional;
}
public get datasourcesOptional(): boolean {
return this.widgetConfigComponent.modelValue?.typeParameters?.datasourcesOptional;
}
public get maxDataKeys(): number {
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys;
}
@ -276,18 +280,20 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
}
private updateValidators() {
const type: DatasourceType = this.datasourceFormGroup.get('type').value;
this.datasourceFormGroup.get('deviceId').setValidators(
type === DatasourceType.device ? [Validators.required] : []
);
this.datasourceFormGroup.get('entityAliasId').setValidators(
(type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : []
);
const newDataKeysRequired = !this.isDataKeysOptional(type);
this.datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []);
this.datasourceFormGroup.get('deviceId').updateValueAndValidity({emitEvent: false});
this.datasourceFormGroup.get('entityAliasId').updateValueAndValidity({emitEvent: false});
this.datasourceFormGroup.get('dataKeys').updateValueAndValidity({emitEvent: false});
if (!this.datasourcesOptional) {
const type: DatasourceType = this.datasourceFormGroup.get('type').value;
this.datasourceFormGroup.get('deviceId').setValidators(
type === DatasourceType.device ? [Validators.required] : []
);
this.datasourceFormGroup.get('entityAliasId').setValidators(
(type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : []
);
const newDataKeysRequired = !this.isDataKeysOptional(type);
this.datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []);
this.datasourceFormGroup.get('deviceId').updateValueAndValidity({emitEvent: false});
this.datasourceFormGroup.get('entityAliasId').updateValueAndValidity({emitEvent: false});
this.datasourceFormGroup.get('dataKeys').updateValueAndValidity({emitEvent: false});
}
}
}

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

@ -30,7 +30,7 @@ import {
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import {
Datasource,
DatasourceType,
DatasourceType, datasourceValid,
JsonSettingsSchema,
WidgetConfigMode,
widgetType
@ -317,6 +317,9 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
}
private datasourcesUpdated(datasources: Datasource[]) {
if (this.datasourcesOptional) {
datasources = datasources ? datasources.filter(d => datasourceValid(d)) : [];
}
this.propagateChange(datasources);
}

17
ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts

@ -33,11 +33,6 @@ import { WidgetSettingsCommonModule } from '@home/components/widget/lib/settings
import { TimewindowStyleComponent } from '@home/components/widget/config/timewindow-style.component';
import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component';
import { TargetDeviceComponent } from '@home/components/widget/config/target-device.component';
import { WidgetActionComponent } from '@home/components/widget/config/action/widget-action.component';
import { CustomActionPrettyResourcesTabsComponent }
from '@home/components/widget/config/action/custom-action-pretty-resources-tabs.component';
import { CustomActionPrettyEditorComponent } from '@home/components/widget/config/action/custom-action-pretty-editor.component';
import { MobileActionEditorComponent } from '@home/components/widget/config/action/mobile-action-editor.component';
@NgModule({
declarations:
@ -55,11 +50,7 @@ import { MobileActionEditorComponent } from '@home/components/widget/config/acti
TimewindowStyleComponent,
TimewindowStylePanelComponent,
TimewindowConfigPanelComponent,
WidgetSettingsComponent,
WidgetActionComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent
WidgetSettingsComponent
],
imports: [
CommonModule,
@ -82,11 +73,7 @@ import { MobileActionEditorComponent } from '@home/components/widget/config/acti
TimewindowStylePanelComponent,
TimewindowConfigPanelComponent,
WidgetSettingsComponent,
WidgetSettingsCommonModule,
WidgetActionComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent
WidgetSettingsCommonModule
]
})
export class WidgetConfigComponentsModule { }

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

@ -23,7 +23,7 @@ import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { DataKey, DatasourceType, KeyInfo, WidgetConfigMode } from '@shared/models/widget.models';
import { DataKey, DatasourceType, KeyInfo, WidgetConfigMode, widgetType } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isDefinedAndNotNull } from '@core/utils';
@ -63,6 +63,18 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
return this.widgetConfigComponent.aliasController;
}
get callbacks(): WidgetConfigCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
}
get widgetEditMode(): boolean {
return this.widgetConfigComponent.widgetEditMode;
}
widgetConfigChangedEmitter = new EventEmitter<WidgetConfigComponentData>();
widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable();

46
ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.scss

@ -0,0 +1,46 @@
/**
* 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.
*/
.tb-action-widget-error-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1;
.tb-action-widget-error-panel {
display: flex;
padding: 4px 4px 4px 12px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 4px;
background-color: #fff2f3;
box-shadow: -2px 2px 4px 0px rgba(0,0,0,0.2);
.tb-action-widget-error-text {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
color: rgba(209, 39, 48, 1);
}
.tb-action-widget-error-clear {
color: rgba(209, 39, 48, 1);
}
}
}

35
ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.html

@ -0,0 +1,35 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-action-button-widget" [style.pointer-events]="ctx.isEdit ? 'none' : 'all'">
<div class="tb-action-button-widget-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<tb-widget-button
[appearance]="appearance"
[borderRadius]="borderRadius"
[disabled]="disabled"
[activated]="activated"
(clicked)="onClick($event)">
</tb-widget-button>
<div *ngIf="error" class="tb-action-widget-error-container">
<div class="tb-action-widget-error-panel">
<div class="tb-action-widget-error-text" [innerHTML]="error | safe: 'html'"></div>
<button class="tb-action-widget-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button>
</div>
</div>
</div>

28
ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.scss

@ -0,0 +1,28 @@
/**
* 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.
*/
.tb-action-button-widget {
width: 100%;
height: 100%;
position: relative;
> div.tb-action-button-widget-title-panel {
position: absolute;
top: 12px;
left: 12px;
right: 12px;
z-index: 2;
}
}

100
ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.ts

@ -0,0 +1,100 @@
///
/// 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 { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { BasicActionWidgetComponent } from '@home/components/widget/lib/action/action-widget.models';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import { ValueType } from '@shared/models/constants';
import {
actionButtonDefaultSettings,
ActionButtonWidgetSettings
} from '@home/components/widget/lib/button/action-button-widget.models';
import { WidgetButtonAppearance } from '@shared/components/button/widget-button.models';
@Component({
selector: 'tb-action-button-widget',
templateUrl: './action-button-widget.component.html',
styleUrls: ['../action/action-widget.scss', './action-button-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ActionButtonWidgetComponent extends
BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
settings: ActionButtonWidgetSettings;
disabled = false;
activated = false;
appearance: WidgetButtonAppearance;
borderRadius = '4px';
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
protected cd: ChangeDetectorRef) {
super(cd);
}
ngOnInit(): void {
super.ngOnInit();
this.settings = {...actionButtonDefaultSettings, ...this.ctx.settings};
this.appearance = this.settings.appearance;
const activatedStateSettings =
{...this.settings.activatedState, actionLabel: this.ctx.translate.instant('widgets.button-state.activated-state')};
this.createValueGetter(activatedStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onActivated(value)
});
const disabledStateSettings =
{...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.button-state.disabled-state')};
this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onDisabled(value)
});
}
ngAfterViewInit(): void {
super.ngAfterViewInit();
}
ngOnDestroy() {
super.ngOnDestroy();
}
public onInit() {
super.onInit();
this.borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.cd.detectChanges();
}
public onClick($event: MouseEvent) {
if (!this.ctx.isEdit && !this.ctx.isPreview) {
this.ctx.actionsApi.click($event);
}
}
private onActivated(value: boolean): void {
this.activated = !!value;
this.cd.markForCheck();
}
private onDisabled(value: boolean): void {
this.disabled = !!value;
this.cd.markForCheck();
}
}

67
ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.models.ts

@ -0,0 +1,67 @@
///
/// 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 {
WidgetButtonAppearance,
widgetButtonDefaultAppearance
} from '@shared/components/button/widget-button.models';
import { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models';
export interface ActionButtonWidgetSettings {
appearance: WidgetButtonAppearance;
activatedState: GetValueSettings<boolean>;
disabledState: GetValueSettings<boolean>;
}
export const actionButtonDefaultSettings: ActionButtonWidgetSettings = {
appearance: widgetButtonDefaultAppearance,
activatedState: {
action: GetValueAction.DO_NOTHING,
defaultValue: false,
getAttribute: {
key: 'state',
scope: null,
subscribeForUpdates: false
},
getTimeSeries: {
key: 'state',
subscribeForUpdates: false
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
disabledState: {
action: GetValueAction.DO_NOTHING,
defaultValue: false,
getAttribute: {
key: 'state',
scope: null,
subscribeForUpdates: false
},
getTimeSeries: {
key: 'state',
subscribeForUpdates: false
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
}
};

8
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html

@ -33,10 +33,10 @@
</div>
</div>
<mat-progress-bar class="tb-single-switch-progress" style="height: 2px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar>
<div *ngIf="error" class="tb-single-switch-error-container">
<div class="tb-single-switch-error-panel">
<div class="tb-single-switch-error-text" [innerHTML]="error | safe: 'html'"></div>
<button class="tb-single-switch-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button>
<div *ngIf="error" class="tb-action-widget-error-container">
<div class="tb-action-widget-error-panel">
<div class="tb-action-widget-error-text" [innerHTML]="error | safe: 'html'"></div>
<button class="tb-action-widget-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button>
</div>
</div>
</div>

30
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.scss

@ -45,36 +45,6 @@ $switchColorDisabled: var(--tb-single-switch-color-disabled, #D5D7E5);
left: 0;
right: 0;
}
.tb-single-switch-error-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.tb-single-switch-error-panel {
display: flex;
padding: 4px 4px 4px 12px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 4px;
background-color: #fff2f3;
box-shadow: -2px 2px 4px 0px rgba(0,0,0,0.2);
.tb-single-switch-error-text {
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
color: rgba(209, 39, 48, 1);
}
.tb-single-switch-error-clear {
color: rgba(209, 39, 48, 1);
}
}
}
> div.tb-single-switch-title-panel {
position: absolute;
top: 12px;

26
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts

@ -42,9 +42,8 @@ import { Observable } from 'rxjs';
import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser';
import cssjs from '@core/css/css';
import { hashCode } from '@core/utils';
import { ValueType } from '@shared/models/constants';
import { UtilsService } from '@core/services/utils.service';
const horizontalLayoutPadding = 48;
const verticalLayoutPadding = 36;
@ -52,7 +51,7 @@ const verticalLayoutPadding = 36;
@Component({
selector: 'tb-single-switch-widget',
templateUrl: './single-switch-widget.component.html',
styleUrls: ['./single-switch-widget.component.scss'],
styleUrls: ['../action/action-widget.scss', './single-switch-widget.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class SingleSwitchWidgetComponent extends
@ -102,9 +101,12 @@ export class SingleSwitchWidgetComponent extends
private onValueSetter: ValueSetter<boolean>;
private offValueSetter: ValueSetter<boolean>;
private singleSwitchCssClass: string;
constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer,
private renderer: Renderer2,
private utils: UtilsService,
protected cd: ChangeDetectorRef,
private elementRef: ElementRef) {
super(cd);
@ -148,25 +150,21 @@ export class SingleSwitchWidgetComponent extends
`--tb-single-switch-color-off: ${this.settings.switchColorOff};\n`+
`--tb-single-switch-color-disabled: ${this.settings.switchColorDisabled};\n`+
`}`;
const cssParser = new cssjs();
cssParser.testMode = false;
const namespace = 'single-switch-' + hashCode(switchVariablesCss);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, switchVariablesCss);
this.renderer.addClass(this.elementRef.nativeElement, namespace);
this.singleSwitchCssClass =
this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement, 'tb-single-switch', switchVariablesCss);
const getInitialStateSettings =
{...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.value-action.initial-state')};
{...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')};
this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onValue(value)
});
const onUpdateStateSettings = {...this.settings.onUpdateState,
actionLabel: this.ctx.translate.instant('widgets.value-action.turn-on')};
actionLabel: this.ctx.translate.instant('widgets.rpc-state.turn-on')};
this.onValueSetter = this.createValueSetter(onUpdateStateSettings);
const offUpdateStateSettings = {...this.settings.offUpdateState,
actionLabel: this.ctx.translate.instant('widgets.value-action.turn-off')};
actionLabel: this.ctx.translate.instant('widgets.rpc-state.turn-off')};
this.offValueSetter = this.createValueSetter(offUpdateStateSettings);
}
@ -190,6 +188,9 @@ export class SingleSwitchWidgetComponent extends
if (this.panelResize$) {
this.panelResize$.disconnect();
}
if (this.singleSwitchCssClass) {
this.utils.clearCssElement(this.renderer, this.singleSwitchCssClass);
}
super.ngOnDestroy();
}
@ -211,7 +212,6 @@ export class SingleSwitchWidgetComponent extends
}
private onValue(value: boolean): void {
console.log(`onValue: ${value}`);
this.value = !!value;
this.cd.markForCheck();
}

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/value-action-settings-button.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/action-settings-button.component.html

@ -16,9 +16,9 @@
-->
<button #matButton [disabled]="disabled"
class="tb-value-action-settings"
class="tb-action-settings-button"
mat-stroked-button color="primary"
(click)="openValueActionSettingsPopup($event, matButton)">
(click)="openActionSettingsPopup($event, matButton)">
<div>
<span>{{ displayValue }}</span>
<mat-icon class="tb-mat-20">edit</mat-icon>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/value-action-settings-button.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/action-settings-button.scss

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.mdc-button.tb-value-action-settings {
.mdc-button.tb-action-settings-button {
width: 100%;
.mdc-button__label {
width: 100%;

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/value-action-settings-panel.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/action-settings-panel.component.scss

@ -15,7 +15,7 @@
*/
@import '../../../../../../../../../scss/constants';
.tb-value-action-settings-panel {
.tb-action-settings-panel {
width: 530px;
display: flex;
flex-direction: column;
@ -23,20 +23,20 @@
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-value-action-settings-panel-content {
.tb-action-settings-panel-content {
display: flex;
flex-direction: column;
gap: 16px;
overflow: auto;
}
.tb-value-action-settings-title {
.tb-action-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-value-action-settings-panel-buttons {
.tb-action-settings-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;

0
ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-editor.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html

2
ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-editor.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss

@ -64,7 +64,7 @@
.gutter.gutter-horizontal {
cursor: col-resize;
background-image: url("../../../../../../../assets/split.js/grips/vertical.png");
background-image: url("../../../../../../../../../assets/split.js/grips/vertical.png");
}
.tb-split.tb-split-horizontal,

4
ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-editor.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts

@ -15,7 +15,7 @@
///
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../../../../../src/typings/split.js.typings.d.ts" />
/// <reference path="../../../../../../../../../../src/typings/split.js.typings.d.ts" />
import {
AfterViewInit,
@ -35,7 +35,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { combineLatest } from 'rxjs';
import { CustomActionDescriptor } from '@shared/models/widget.models';
import { CustomPrettyActionEditorCompleter } from '@home/components/widget/config/action/custom-action.models';
import { CustomPrettyActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models';
@Component({
selector: 'tb-custom-action-pretty-editor',

0
ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-resources-tabs.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html

0
ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-resources-tabs.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.scss

2
ui-ngx/src/app/modules/home/components/widget/config/action/custom-action-pretty-resources-tabs.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts

@ -35,7 +35,7 @@ import { CustomActionDescriptor } from '@shared/models/widget.models';
import { Ace } from 'ace-builds';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { ResizeObserver } from '@juggle/resize-observer';
import { CustomPrettyActionEditorCompleter } from '@home/components/widget/config/action/custom-action.models';
import { CustomPrettyActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models';
import { Observable } from 'rxjs/internal/Observable';
import { forkJoin, from } from 'rxjs';
import { map, tap } from 'rxjs/operators';

0
ui-ngx/src/app/modules/home/components/widget/config/action/custom-action.models.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action.models.ts

0
ui-ngx/src/app/modules/home/components/widget/config/action/custom-sample-css.raw → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-sample-css.raw

0
ui-ngx/src/app/modules/home/components/widget/config/action/custom-sample-html.raw → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-sample-html.raw

0
ui-ngx/src/app/modules/home/components/widget/config/action/custom-sample-js.raw → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-sample-js.raw

14
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html

@ -15,9 +15,9 @@
limitations under the License.
-->
<div class="tb-value-action-settings-panel" [formGroup]="getValueSettingsFormGroup">
<div class="tb-value-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-value-action-settings-panel-content">
<div class="tb-action-settings-panel" [formGroup]="getValueSettingsFormGroup">
<div class="tb-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-action-settings-panel-content">
<div class="tb-form-row">
<div class="fixed-title-width" >{{ 'widgets.value-action.action' | translate }}</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
@ -33,8 +33,8 @@
<div class="tb-form-row space-between">
<div tb-hint-tooltip-icon="{{'widgets.value-action.init-value-hint' | translate}}" translate>widgets.value-action.value</div>
<tb-value-input [valueType]="valueType"
trueLabel="widgets.value-action.on"
falseLabel="widgets.value-action.off"
[trueLabel]="trueLabel"
[falseLabel]="falseLabel"
formControlName="defaultValue"></tb-value-input>
</div>
</ng-template>
@ -132,7 +132,7 @@
helpId="widget/lib/rpc/parse_value_fn">
</tb-js-func>
<div *ngIf="valueType === ValueType.BOOLEAN" class="tb-form-row align-start no-gap column-xs">
<div class="fixed-title-width fixed-title-height" translate>widgets.value-action.on-when-result-is</div>
<div class="fixed-title-width fixed-title-height">{{ 'widgets.value-action.state-when-result-is' | translate:{state: (stateLabel | translate)} }}</div>
<tb-value-input
fxFlex
layout="column"
@ -203,7 +203,7 @@
</mat-expansion-panel>
</div>
</div>
<div class="tb-value-action-settings-panel-buttons">
<div class="tb-action-settings-panel-buttons">
<button mat-button
color="primary"
type="button"

21
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts

@ -24,12 +24,12 @@ import { merge } from 'rxjs';
import {
DataToValueType,
GetValueAction,
getValueActions,
getValueActions, getValueActionsByWidgetType,
getValueActionTranslations,
GetValueSettings
} from '@shared/models/action-widget-settings.models';
import { ValueType } from '@shared/models/constants';
import { TargetDevice } from '@shared/models/widget.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetService } from '@core/http/widget.service';
@ -38,7 +38,7 @@ import { WidgetService } from '@core/http/widget.service';
selector: 'tb-get-value-action-settings-panel',
templateUrl: './get-value-action-settings-panel.component.html',
providers: [],
styleUrls: ['./value-action-settings-panel.component.scss'],
styleUrls: ['./action-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class GetValueActionSettingsPanelComponent extends PageComponent implements OnInit {
@ -52,12 +52,24 @@ export class GetValueActionSettingsPanelComponent extends PageComponent implemen
@Input()
valueType: ValueType;
@Input()
trueLabel = 'value.true';
@Input()
falseLabel = 'value.false';
@Input()
stateLabel: string;
@Input()
aliasController: IAliasController;
@Input()
targetDevice: TargetDevice;
@Input()
widgetType: widgetType;
@Input()
popover: TbPopoverComponent<GetValueActionSettingsPanelComponent>;
@ -66,7 +78,7 @@ export class GetValueActionSettingsPanelComponent extends PageComponent implemen
getValueAction = GetValueAction;
getValueActions = getValueActions;
getValueActions: GetValueAction[];
getValueActionTranslationsMap = getValueActionTranslations;
@ -91,6 +103,7 @@ export class GetValueActionSettingsPanelComponent extends PageComponent implemen
}
ngOnInit(): void {
this.getValueActions = getValueActionsByWidgetType(this.widgetType);
this.getValueSettingsFormGroup = this.fb.group(
{
action: [this.getValueSettings?.action, []],

29
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts

@ -35,12 +35,12 @@ import {
GetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component';
import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice } from '@shared/models/widget.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models';
@Component({
selector: 'tb-get-value-action-settings',
templateUrl: './value-action-settings-button.component.html',
styleUrls: ['./value-action-settings-button.scss'],
templateUrl: './action-settings-button.component.html',
styleUrls: ['./action-settings-button.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -61,12 +61,24 @@ export class GetValueActionSettingsComponent implements OnInit, ControlValueAcce
@Input()
valueType: ValueType;
@Input()
trueLabel = 'value.true';
@Input()
falseLabel = 'value.false';
@Input()
stateLabel: string;
@Input()
aliasController: IAliasController;
@Input()
targetDevice: TargetDevice;
@Input()
widgetType: widgetType;
@Input()
disabled = false;
@ -103,7 +115,7 @@ export class GetValueActionSettingsComponent implements OnInit, ControlValueAcce
this.updateDisplayValue();
}
openValueActionSettingsPopup($event: Event, matButton: MatButton) {
openActionSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
@ -115,8 +127,12 @@ export class GetValueActionSettingsComponent implements OnInit, ControlValueAcce
getValueSettings: this.modelValue,
panelTitle: this.panelTitle,
valueType: this.valueType,
trueLabel: this.trueLabel,
falseLabel: this.falseLabel,
stateLabel: this.stateLabel,
aliasController: this.aliasController,
targetDevice: this.targetDevice
targetDevice: this.targetDevice,
widgetType: this.widgetType
};
const getValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, GetValueActionSettingsPanelComponent,
@ -138,7 +154,8 @@ export class GetValueActionSettingsComponent implements OnInit, ControlValueAcce
switch (this.modelValue.action) {
case GetValueAction.DO_NOTHING:
if (this.valueType === ValueType.BOOLEAN) {
this.displayValue = this.translate.instant(!!this.modelValue.defaultValue ? 'widgets.value-action.on' : 'widgets.value-action.off');
this.displayValue =
this.translate.instant(!!this.modelValue.defaultValue ? this.trueLabel : this.falseLabel);
} else {
this.displayValue = this.modelValue.defaultValue + '';
}

0
ui-ngx/src/app/modules/home/components/widget/config/action/mobile-action-editor.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html

4
ui-ngx/src/app/modules/home/components/widget/config/action/mobile-action-editor.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts

@ -29,7 +29,7 @@ import {
WidgetMobileActionType,
widgetMobileActionTypeTranslationMap
} from '@shared/models/widget.models';
import { CustomActionEditorCompleter } from '@home/components/widget/config/action/custom-action.models';
import { CustomActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models';
import {
getDefaultGetLocationFunction,
getDefaultGetPhoneNumberFunction,
@ -39,7 +39,7 @@ import {
getDefaultProcessLaunchResultFunction,
getDefaultProcessLocationFunction,
getDefaultProcessQrCodeFunction
} from '@home/components/widget/config/action/mobile-action-editor.models';
} from '@home/components/widget/lib/settings/common/action/mobile-action-editor.models';
import { WidgetService } from '@core/http/widget.service';
@Component({

0
ui-ngx/src/app/modules/home/components/widget/config/action/mobile-action-editor.models.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html

@ -15,9 +15,9 @@
limitations under the License.
-->
<div class="tb-value-action-settings-panel" [formGroup]="setValueSettingsFormGroup">
<div class="tb-value-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-value-action-settings-panel-content">
<div class="tb-action-settings-panel" [formGroup]="setValueSettingsFormGroup">
<div class="tb-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-action-settings-panel-content">
<div class="tb-form-row">
<div class="fixed-title-width" >{{ 'widgets.value-action.action' | translate }}</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
@ -183,7 +183,7 @@
</mat-expansion-panel>
</div>
</div>
<div class="tb-value-action-settings-panel-buttons">
<div class="tb-action-settings-panel-buttons">
<button mat-button
color="primary"
type="button"

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts

@ -24,13 +24,13 @@ import { merge } from 'rxjs';
import {
getValueActions,
SetValueAction,
setValueActions,
setValueActions, setValueActionsByWidgetType,
setValueActionTranslations,
SetValueSettings,
ValueToDataType
} from '@shared/models/action-widget-settings.models';
import { ValueType } from '@shared/models/constants';
import { TargetDevice } from '@shared/models/widget.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetService } from '@core/http/widget.service';
@ -39,7 +39,7 @@ import { WidgetService } from '@core/http/widget.service';
selector: 'tb-set-value-action-settings-panel',
templateUrl: './set-value-action-settings-panel.component.html',
providers: [],
styleUrls: ['./value-action-settings-panel.component.scss'],
styleUrls: ['./action-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class SetValueActionSettingsPanelComponent extends PageComponent implements OnInit {
@ -59,6 +59,9 @@ export class SetValueActionSettingsPanelComponent extends PageComponent implemen
@Input()
targetDevice: TargetDevice;
@Input()
widgetType: widgetType;
@Input()
popover: TbPopoverComponent<SetValueActionSettingsPanelComponent>;
@ -67,7 +70,7 @@ export class SetValueActionSettingsPanelComponent extends PageComponent implemen
setValueAction = SetValueAction;
setValueActions = setValueActions;
setValueActions: SetValueAction[];
setValueActionTranslationsMap = setValueActionTranslations;
@ -92,6 +95,7 @@ export class SetValueActionSettingsPanelComponent extends PageComponent implemen
}
ngOnInit(): void {
this.setValueActions = setValueActionsByWidgetType(this.widgetType);
this.setValueSettingsFormGroup = this.fb.group(
{
action: [this.setValueSettings?.action, []],

14
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts

@ -32,7 +32,7 @@ import { SetValueAction, SetValueSettings, ValueToDataType } from '@shared/model
import { TranslateService } from '@ngx-translate/core';
import { ValueType } from '@shared/models/constants';
import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice } from '@shared/models/widget.models';
import { TargetDevice, widgetType } from '@shared/models/widget.models';
import { isDefinedAndNotNull } from '@core/utils';
import {
SetValueActionSettingsPanelComponent
@ -40,8 +40,8 @@ import {
@Component({
selector: 'tb-set-value-action-settings',
templateUrl: './value-action-settings-button.component.html',
styleUrls: ['./value-action-settings-button.scss'],
templateUrl: './action-settings-button.component.html',
styleUrls: ['./action-settings-button.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -68,6 +68,9 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce
@Input()
targetDevice: TargetDevice;
@Input()
widgetType: widgetType;
@Input()
disabled = false;
@ -104,7 +107,7 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce
this.updateDisplayValue();
}
openValueActionSettingsPopup($event: Event, matButton: MatButton) {
openActionSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
@ -117,7 +120,8 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce
panelTitle: this.panelTitle,
valueType: this.valueType,
aliasController: this.aliasController,
targetDevice: this.targetDevice
targetDevice: this.targetDevice,
widgetType: this.widgetType
};
const setValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, SetValueActionSettingsPanelComponent,

42
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html

@ -0,0 +1,42 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-action-settings-panel" [formGroup]="widgetActionFormGroup">
<div class="tb-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-action-settings-panel-content">
<tb-widget-action
formControlName="widgetAction"
[callbacks]="callbacks"
[widgetType]="widgetType">
</tb-widget-action>
</div>
<div class="tb-action-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyWidgetAction()"
[disabled]="widgetActionFormGroup.invalid || !widgetActionFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

88
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.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.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import {
DataToValueType,
GetValueAction,
getValueActions,
getValueActionTranslations,
GetValueSettings
} from '@shared/models/action-widget-settings.models';
import { ValueType } from '@shared/models/constants';
import { TargetDevice, WidgetAction, widgetType } from '@shared/models/widget.models';
import { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
import { IAliasController } from '@core/api/widget-api.models';
import { WidgetService } from '@core/http/widget.service';
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models';
@Component({
selector: 'tb-widget-action-settings-panel',
templateUrl: './widget-action-settings-panel.component.html',
providers: [],
styleUrls: ['./action-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class WidgetActionSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
widgetAction: WidgetAction;
@Input()
panelTitle: string;
@Input()
widgetType: widgetType;
@Input()
callbacks: WidgetActionCallbacks;
@Input()
popover: TbPopoverComponent<WidgetActionSettingsPanelComponent>;
@Output()
widgetActionApplied = new EventEmitter<WidgetAction>();
widgetActionFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.widgetActionFormGroup = this.fb.group(
{
widgetAction: [this.widgetAction, []]
}
);
}
cancel() {
this.popover?.hide();
}
applyWidgetAction() {
const widgetAction: WidgetAction = this.widgetActionFormGroup.get('widgetAction').getRawValue();
this.widgetActionApplied.emit(widgetAction);
}
}

136
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts

@ -0,0 +1,136 @@
///
/// 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,
forwardRef,
HostBinding,
Input,
OnInit,
Renderer2,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { TranslateService } from '@ngx-translate/core';
import { WidgetAction, widgetActionTypeTranslationMap, widgetType } from '@shared/models/widget.models';
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models';
import {
WidgetActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/action/widget-action-settings-panel.component';
@Component({
selector: 'tb-widget-action-settings',
templateUrl: './action-settings-button.component.html',
styleUrls: ['./action-settings-button.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetActionSettingsComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class WidgetActionSettingsComponent implements OnInit, ControlValueAccessor {
@HostBinding('style.overflow')
overflow = 'hidden';
@Input()
panelTitle: string;
@Input()
widgetType: widgetType;
@Input()
callbacks: WidgetActionCallbacks;
@Input()
disabled = false;
modelValue: WidgetAction;
displayValue: string;
private propagateChange = null;
constructor(private translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
if (this.disabled !== isDisabled) {
this.disabled = isDisabled;
}
}
writeValue(value: WidgetAction): void {
this.modelValue = value;
this.updateDisplayValue();
}
openActionSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
widgetAction: this.modelValue,
panelTitle: this.panelTitle,
widgetType: this.widgetType,
callbacks: this.callbacks
};
const widgetActionSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, WidgetActionSettingsPanelComponent,
['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
widgetActionSettingsPanelPopover.tbComponentRef.instance.popover = widgetActionSettingsPanelPopover;
widgetActionSettingsPanelPopover.tbComponentRef.instance.widgetActionApplied.subscribe((widgetAction) => {
widgetActionSettingsPanelPopover.hide();
this.modelValue = widgetAction;
this.updateDisplayValue();
this.propagateChange(this.modelValue);
});
}
}
private updateDisplayValue() {
this.displayValue = this.translate.instant(widgetActionTypeTranslationMap.get(this.modelValue.type));
this.cd.markForCheck();
}
}

0
ui-ngx/src/app/modules/home/components/widget/config/action/widget-action.component.html → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html

2
ui-ngx/src/app/modules/home/components/widget/config/action/widget-action.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts

@ -44,7 +44,7 @@ import { PopoverPlacement, PopoverPlacements } from '@shared/components/popover.
import {
CustomActionEditorCompleter,
toCustomAction
} from '@home/components/widget/config/action/custom-action.models';
} from '@home/components/widget/lib/settings/common/action/custom-action.models';
const stateDisplayTypes = ['normal', 'separateDialog', 'popover'] as const;
type stateDisplayTypeTuple = typeof stateDisplayTypes;

97
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html

@ -0,0 +1,97 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-form-panel no-padding no-border" [formGroup]="appearanceFormGroup">
<tb-image-cards-select rowHeight="3:1"
[cols]="{columns: 2,
breakpoints: {
'lt-sm': 1
}}"
label="{{ 'widgets.button.layout' | translate }}" formControlName="type">
<tb-image-cards-select-option *ngFor="let type of widgetButtonTypes"
[value]="type"
[image]="widgetButtonTypeImageMap.get(type)">
{{ widgetButtonTypeTranslationMap.get(type) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.button.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.button.label' | translate }}
</mat-slide-toggle>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.button.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field appearance="outline" class="flex number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
iconClearButton
formControlName="icon">
</tb-material-icon-select>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.button.color-palette' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.button.main</div>
<tb-color-input asBoxInput
formControlName="mainColor">
</tb-color-input>
</div>
<mat-divider vertical></mat-divider>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div translate>widgets.button.background</div>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
</div>
<div class="tb-form-panel stroked" formGroupName="customStyle">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<div class="tb-form-panel-title" translate>widgets.button.custom-styles</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row space-between" *ngFor="let state of widgetButtonStates">
<div>{{ widgetButtonStateTranslationMap.get(state) | translate }}</div>
<tb-widget-button-custom-style
[state]="state"
[appearance]="this.appearanceFormGroup.value"
[borderRadius]="borderRadius"
[formControlName]="state">
</tb-widget-button-custom-style>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</div>

141
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.ts

@ -0,0 +1,141 @@
///
/// 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, forwardRef, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import {
WidgetButtonAppearance,
widgetButtonStates, widgetButtonStatesTranslations,
widgetButtonTypeImages,
widgetButtonTypes,
widgetButtonTypeTranslations
} from '@shared/components/button/widget-button.models';
import { merge } from 'rxjs';
@Component({
selector: 'tb-widget-button-appearance',
templateUrl: './widget-button-appearance.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetButtonAppearanceComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class WidgetButtonAppearanceComponent implements OnInit, ControlValueAccessor {
@Input()
disabled = false;
@Input()
borderRadius: string;
widgetButtonTypes = widgetButtonTypes;
widgetButtonTypeTranslationMap = widgetButtonTypeTranslations;
widgetButtonTypeImageMap = widgetButtonTypeImages;
widgetButtonStates = widgetButtonStates;
widgetButtonStateTranslationMap = widgetButtonStatesTranslations;
modelValue: WidgetButtonAppearance;
appearanceFormGroup: UntypedFormGroup;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder) {}
ngOnInit(): void {
this.appearanceFormGroup = this.fb.group({
type: [null, []],
autoScale: [null, []],
showLabel: [null, []],
label: [null, []],
showIcon: [null, []],
icon: [null, []],
iconSize: [null, []],
iconSizeUnit: [null, []],
mainColor: [null, []],
backgroundColor: [null, []]
});
const customStyle = this.fb.group({});
for (const state of widgetButtonStates) {
customStyle.addControl(state, this.fb.control(null, []));
}
this.appearanceFormGroup.addControl('customStyle', customStyle);
this.appearanceFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
merge(this.appearanceFormGroup.get('showLabel').valueChanges,
this.appearanceFormGroup.get('showIcon').valueChanges)
.subscribe(() => {
this.updateValidators();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.appearanceFormGroup.disable({emitEvent: false});
} else {
this.appearanceFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(value: WidgetButtonAppearance): void {
this.modelValue = value;
this.appearanceFormGroup.patchValue(
value, {emitEvent: false}
);
this.updateValidators();
}
private updateModel() {
this.modelValue = this.appearanceFormGroup.getRawValue();
this.propagateChange(this.modelValue);
}
private updateValidators(): void {
const showLabel: boolean = this.appearanceFormGroup.get('showLabel').value;
const showIcon: boolean = this.appearanceFormGroup.get('showIcon').value;
if (showLabel) {
this.appearanceFormGroup.get('label').enable();
} else {
this.appearanceFormGroup.get('label').disable();
}
if (showIcon) {
this.appearanceFormGroup.get('icon').enable();
this.appearanceFormGroup.get('iconSize').enable();
this.appearanceFormGroup.get('iconSizeUnit').enable();
} else {
this.appearanceFormGroup.get('icon').disable();
this.appearanceFormGroup.get('iconSize').disable();
this.appearanceFormGroup.get('iconSizeUnit').disable();
}
}
}

93
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.html

@ -0,0 +1,93 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-widget-button-custom-style-panel" [formGroup]="customStyleFormGroup">
<div class="tb-widget-button-custom-style-title">{{ widgetButtonStateTranslationMap.get(state) | translate }}</div>
<div class="tb-widget-button-custom-style-panel-content">
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="overrideMainColor">
{{ 'widgets.button.main' | translate }}
</mat-slide-toggle>
<tb-color-input asBoxInput
formControlName="mainColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="overrideBackgroundColor">
{{ 'widgets.button.background' | translate }}
</mat-slide-toggle>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide" formControlName="overrideDropShadow">
{{ 'widgets.button.shadow' | translate }}
</mat-slide-toggle>
<mat-chip-listbox formControlName="dropShadow">
<mat-chip-option [selectable]="!customStyleFormGroup.get('dropShadow').value" [value]="true">{{ 'widgets.button.enabled' | translate }}</mat-chip-option>
<mat-chip-option [selectable]="customStyleFormGroup.get('dropShadow').value" [value]="false">{{ 'widgets.button.disabled' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
<div class="tb-widget-button-custom-style-preview">
<div class="tb-widget-button-custom-style-preview-title" translate>
widgets.button.preview
</div>
<tb-widget-button
[appearance]="previewAppearance"
[borderRadius]="borderRadius"
disableEvents
[hovered]="state === widgetButtonState.hovered"
[pressed]="state === widgetButtonState.pressed"
[activated]="state === widgetButtonState.activated"
[disabled]="state === widgetButtonState.disabled">
</tb-widget-button>
</div>
</div>
<div class="tb-widget-button-custom-style-panel-buttons">
<button *ngIf="copyFromStates?.length"
#copyStyleButton
class="tb-nowrap"
mat-stroked-button
color="primary"
type="button"
[matMenuTriggerFor]="styleSourcesMenu" [matMenuTriggerData]="{menuWidth: copyStyleButton._elementRef.nativeElement.clientWidth}">
{{ 'widgets.button.copy-style-from' | translate }}
</button>
<mat-menu #styleSourcesMenu="matMenu">
<ng-template matMenuContent let-menuWidth="menuWidth">
<div [style.min-width.px]="menuWidth">
<button mat-menu-item *ngFor="let state of copyFromStates" (click)="copyStyle(state)">{{ widgetButtonStateTranslationMap.get(state) | translate }}</button>
</div>
</ng-template>
</mat-menu>
<span fxFlex></span>
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyCustomStyle()"
[disabled]="customStyleFormGroup.invalid || !customStyleFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

68
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.scss

@ -0,0 +1,68 @@
/**
* 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-widget-button-custom-style-panel {
width: 530px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-lt-md} {
width: 90vw;
}
.tb-widget-button-custom-style-panel-content {
display: flex;
flex-direction: column;
gap: 16px;
overflow: auto;
}
.tb-widget-button-custom-style-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-widget-button-custom-style-preview {
flex: 1;
background: rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
padding: 12px 16px 24px 16px;
align-items: center;
gap: 12px;
.tb-widget-button-custom-style-preview-title {
align-self: stretch;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
color: rgba(0, 0, 0, 0.38);
}
tb-widget-button {
width: 200px;
height: 60px;
}
}
.tb-widget-button-custom-style-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

171
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.ts

@ -0,0 +1,171 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
defaultBackgroundColorDisabled,
defaultMainColorDisabled,
WidgetButtonAppearance,
WidgetButtonCustomStyle,
WidgetButtonState,
widgetButtonStates,
widgetButtonStatesTranslations,
WidgetButtonType
} from '@shared/components/button/widget-button.models';
import { merge } from 'rxjs';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-widget-button-custom-style-panel',
templateUrl: './widget-button-custom-style-panel.component.html',
providers: [],
styleUrls: ['./widget-button-custom-style-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class WidgetButtonCustomStylePanelComponent extends PageComponent implements OnInit {
@Input()
appearance: WidgetButtonAppearance;
@Input()
borderRadius: string;
@Input()
state: WidgetButtonState;
@Input()
customStyle: WidgetButtonCustomStyle;
@Input()
popover: TbPopoverComponent<WidgetButtonCustomStylePanelComponent>;
@Output()
customStyleApplied = new EventEmitter<WidgetButtonCustomStyle>();
widgetButtonStateTranslationMap = widgetButtonStatesTranslations;
widgetButtonState = WidgetButtonState;
previewAppearance: WidgetButtonAppearance;
copyFromStates: WidgetButtonState[];
customStyleFormGroup: UntypedFormGroup;
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>,
private cd: ChangeDetectorRef) {
super(store);
}
ngOnInit(): void {
this.copyFromStates = widgetButtonStates.filter(state =>
state !== this.state && !!this.appearance.customStyle[state]);
this.customStyleFormGroup = this.fb.group(
{
overrideMainColor: [false, []],
mainColor: [null, []],
overrideBackgroundColor: [false, []],
backgroundColor: [null, []],
overrideDropShadow: [false, []],
dropShadow: [false, []]
}
);
merge(this.customStyleFormGroup.get('overrideMainColor').valueChanges,
this.customStyleFormGroup.get('overrideBackgroundColor').valueChanges,
this.customStyleFormGroup.get('overrideDropShadow').valueChanges)
.subscribe(() => {
this.updateValidators();
});
this.customStyleFormGroup.valueChanges.subscribe(() => {
this.updatePreviewAppearance();
});
this.setStyle(this.customStyle);
}
copyStyle(state: WidgetButtonState) {
this.customStyle = deepClone(this.appearance.customStyle[state]);
this.setStyle(this.customStyle);
this.customStyleFormGroup.markAsDirty();
}
cancel() {
this.popover?.hide();
}
applyCustomStyle() {
const customStyle: WidgetButtonCustomStyle = this.customStyleFormGroup.value;
this.customStyleApplied.emit(customStyle);
}
private setStyle(customStyle?: WidgetButtonCustomStyle): void {
let mainColor = this.state === WidgetButtonState.disabled ? defaultMainColorDisabled : this.appearance.mainColor;
if (customStyle?.overrideMainColor) {
mainColor = customStyle?.mainColor;
}
let backgroundColor = this.state === WidgetButtonState.disabled ? defaultBackgroundColorDisabled : this.appearance.backgroundColor;
if (customStyle?.overrideBackgroundColor) {
backgroundColor = customStyle?.backgroundColor;
}
let dropShadow = this.appearance.type === WidgetButtonType.basic ? false : true;
if (customStyle?.overrideDropShadow) {
dropShadow = customStyle?.dropShadow;
}
this.customStyleFormGroup.patchValue({
overrideMainColor: customStyle?.overrideMainColor,
mainColor,
overrideBackgroundColor: customStyle?.overrideBackgroundColor,
backgroundColor,
overrideDropShadow: customStyle?.overrideDropShadow,
dropShadow
}, {emitEvent: false});
this.updateValidators();
this.updatePreviewAppearance();
}
private updateValidators() {
const overrideMainColor: boolean = this.customStyleFormGroup.get('overrideMainColor').value;
const overrideBackgroundColor: boolean = this.customStyleFormGroup.get('overrideBackgroundColor').value;
const overrideDropShadow: boolean = this.customStyleFormGroup.get('overrideDropShadow').value;
if (overrideMainColor) {
this.customStyleFormGroup.get('mainColor').enable({emitEvent: false});
} else {
this.customStyleFormGroup.get('mainColor').disable({emitEvent: false});
}
if (overrideBackgroundColor) {
this.customStyleFormGroup.get('backgroundColor').enable({emitEvent: false});
} else {
this.customStyleFormGroup.get('backgroundColor').disable({emitEvent: false});
}
if (overrideDropShadow) {
this.customStyleFormGroup.get('dropShadow').enable({emitEvent: false});
} else {
this.customStyleFormGroup.get('dropShadow').disable({emitEvent: false});
}
}
private updatePreviewAppearance() {
this.previewAppearance = {...this.appearance};
this.previewAppearance.customStyle[this.state] = this.customStyleFormGroup.value;
this.cd.markForCheck();
}
}

44
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html

@ -0,0 +1,44 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-widget-button-custom-style">
<div class="tb-widget-button-preview-panel tb-primary-fill">
<tb-widget-button
[appearance]="previewAppearance"
[borderRadius]="borderRadius"
disableEvents
[hovered]="state === widgetButtonState.hovered"
[pressed]="state === widgetButtonState.pressed"
[activated]="state === widgetButtonState.activated"
[disabled]="state === widgetButtonState.disabled">
</tb-widget-button>
<button *ngIf="modelValue"
mat-icon-button
[matTooltip]="'widgets.button.clear-style' | translate"
matTooltipPosition="above"
class="tb-mat-32"
(click)="clearStyle()">
<tb-icon>mdi:broom</tb-icon>
</button>
</div>
<button mat-icon-button
class="tb-mat-32"
#matIconButton
(click)="openButtonCustomStylePopup($event, matIconButton)">
<mat-icon>edit</mat-icon>
</button>
</div>

46
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.scss

@ -0,0 +1,46 @@
/**
* 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-widget-button-custom-style {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
button.mat-mdc-icon-button {
color: rgba(0,0,0,0.56);
}
.tb-widget-button-preview-panel {
width: 148px;
height: 48px;
padding: 8px 12px;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
tb-widget-button {
width: 84px;
height: 100%;
}
@media #{$mat-gt-xs} {
width: 168px;
tb-widget-button {
width: 104px;
}
}
}
}

157
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts

@ -0,0 +1,157 @@
///
/// 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,
forwardRef,
Input,
OnChanges,
OnInit,
Renderer2,
SimpleChanges,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
WidgetButtonAppearance,
WidgetButtonCustomStyle,
WidgetButtonState
} from '@shared/components/button/widget-button.models';
import { TbPopoverService } from '@shared/components/popover.service';
import { MatIconButton } from '@angular/material/button';
import {
WidgetButtonCustomStylePanelComponent
} from '@home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component';
@Component({
selector: 'tb-widget-button-custom-style',
templateUrl: './widget-button-custom-style.component.html',
styleUrls: ['./widget-button-custom-style.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetButtonCustomStyleComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class WidgetButtonCustomStyleComponent implements OnInit, OnChanges, ControlValueAccessor {
@Input()
disabled = false;
@Input()
appearance: WidgetButtonAppearance;
@Input()
borderRadius: string;
@Input()
state: WidgetButtonState;
widgetButtonState = WidgetButtonState;
modelValue: WidgetButtonCustomStyle;
previewAppearance: WidgetButtonAppearance;
private propagateChange = (_val: any) => {};
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) {}
ngOnInit(): void {
this.updatePreviewAppearance();
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange) {
if (propName === 'appearance') {
this.updatePreviewAppearance();
}
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(_isDisabled: boolean): void {
}
writeValue(value: WidgetButtonCustomStyle): void {
this.modelValue = value;
this.updatePreviewAppearance();
}
clearStyle() {
this.updateModel(null);
}
openButtonCustomStylePopup($event: Event, matButton: MatIconButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
appearance: this.appearance,
borderRadius: this.borderRadius,
state: this.state,
customStyle: this.modelValue
};
const widgetButtonCustomStylePanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, WidgetButtonCustomStylePanelComponent,
['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
ctx,
{},
{}, {}, true);
widgetButtonCustomStylePanelPopover.tbComponentRef.instance.popover = widgetButtonCustomStylePanelPopover;
widgetButtonCustomStylePanelPopover.tbComponentRef.instance.customStyleApplied.subscribe((customStyle) => {
widgetButtonCustomStylePanelPopover.hide();
this.updateModel(customStyle);
});
}
}
private updateModel(value: WidgetButtonCustomStyle): void {
this.modelValue = value;
this.updatePreviewAppearance();
this.propagateChange(this.modelValue);
}
private updatePreviewAppearance() {
this.previewAppearance = {...this.appearance};
if (this.modelValue) {
this.previewAppearance.customStyle[this.state] = this.modelValue;
}
this.cd.markForCheck();
}
}

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

@ -67,6 +67,31 @@ import {
SetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component';
import { CssSizeInputComponent } from '@home/components/widget/lib/settings/common/css-size-input.component';
import { WidgetActionComponent } from '@home/components/widget/lib/settings/common/action/widget-action.component';
import {
CustomActionPrettyResourcesTabsComponent
} from '@home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component';
import {
CustomActionPrettyEditorComponent
} from '@home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component';
import {
MobileActionEditorComponent
} from '@home/components/widget/lib/settings/common/action/mobile-action-editor.component';
import {
WidgetActionSettingsComponent
} from '@home/components/widget/lib/settings/common/action/widget-action-settings.component';
import {
WidgetActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/action/widget-action-settings-panel.component';
import {
WidgetButtonAppearanceComponent
} from '@home/components/widget/lib/settings/common/button/widget-button-appearance.component';
import {
WidgetButtonCustomStyleComponent
} from '@home/components/widget/lib/settings/common/button/widget-button-custom-style.component';
import {
WidgetButtonCustomStylePanelComponent
} from '@home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component';
@NgModule({
declarations: [
@ -93,7 +118,16 @@ import { CssSizeInputComponent } from '@home/components/widget/lib/settings/comm
GetValueActionSettingsPanelComponent,
DeviceKeyAutocompleteComponent,
SetValueActionSettingsComponent,
SetValueActionSettingsPanelComponent
SetValueActionSettingsPanelComponent,
WidgetActionComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent,
WidgetActionSettingsComponent,
WidgetActionSettingsPanelComponent,
WidgetButtonAppearanceComponent,
WidgetButtonCustomStyleComponent,
WidgetButtonCustomStylePanelComponent
],
imports: [
CommonModule,
@ -124,7 +158,16 @@ import { CssSizeInputComponent } from '@home/components/widget/lib/settings/comm
GetValueActionSettingsPanelComponent,
DeviceKeyAutocompleteComponent,
SetValueActionSettingsComponent,
SetValueActionSettingsPanelComponent
SetValueActionSettingsPanelComponent,
WidgetActionComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent,
WidgetActionSettingsComponent,
WidgetActionSettingsPanelComponent,
WidgetButtonAppearanceComponent,
WidgetButtonCustomStyleComponent,
WidgetButtonCustomStylePanelComponent
],
providers: [
ColorSettingsComponentService,

18
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html

@ -19,30 +19,36 @@
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.single-switch.behavior</div>
<div class="tb-form-row">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.initial-state-hint' | translate}}" translate>widgets.value-action.initial-state</div>
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.initial-state-hint' | translate}}" translate>widgets.rpc-state.initial-state</div>
<tb-get-value-action-settings fxFlex
panelTitle="widgets.value-action.initial-state"
panelTitle="widgets.rpc-state.initial-state"
[valueType]="valueType.BOOLEAN"
trueLabel="widgets.rpc-state.on"
falseLabel="widgets.rpc-state.off"
stateLabel="widgets.rpc-state.on"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="initialState"></tb-get-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-on-hint' | translate}}" translate>widgets.value-action.turn-on</div>
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-on-hint' | translate}}" translate>widgets.rpc-state.turn-on</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.value-action.turn-on"
panelTitle="widgets.rpc-state.turn-on"
[valueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="onUpdateState"></tb-set-value-action-settings>
</div>
<div class="tb-form-row space-between">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.value-action.turn-off-hint' | translate}}" translate>widgets.value-action.turn-off</div>
<div class="fixed-title-width" tb-hint-tooltip-icon="{{'widgets.rpc-state.turn-off-hint' | translate}}" translate>widgets.rpc-state.turn-off</div>
<tb-set-value-action-settings fxFlex
panelTitle="widgets.value-action.turn-off"
panelTitle="widgets.rpc-state.turn-off"
[valueType]="valueType.BOOLEAN"
[aliasController]="aliasController"
[targetDevice]="targetDevice"
[widgetType]="widgetType"
formControlName="offUpdateState"></tb-set-value-action-settings>
</div>
</div>

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts

@ -15,7 +15,7 @@
///
import { Component } from '@angular/core';
import { TargetDevice, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -37,6 +37,10 @@ export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent
return this.widget?.config?.targetDevice;
}
get widgetType(): widgetType {
return this.widget?.type;
}
singleSwitchLayouts = singleSwitchLayouts;
singleSwitchLayoutTranslationMap = singleSwitchLayoutTranslations;

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

@ -565,6 +565,9 @@ export class WidgetComponentService {
if (isUndefined(result.typeParameters.embedTitlePanel)) {
result.typeParameters.embedTitlePanel = false;
}
if (isUndefined(result.typeParameters.overflowVisible)) {
result.typeParameters.overflowVisible = false;
}
if (isUndefined(result.typeParameters.hideDataSettings)) {
result.typeParameters.hideDataSettings = false;
}

7
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -71,6 +71,7 @@ import {
BarChartWithLabelsWidgetComponent
} from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.component';
import { SingleSwitchWidgetComponent } from '@home/components/widget/lib/rpc/single-switch-widget.component';
import { ActionButtonWidgetComponent } from '@home/components/widget/lib/button/action-button-widget.component';
@NgModule({
declarations:
@ -114,7 +115,8 @@ import { SingleSwitchWidgetComponent } from '@home/components/widget/lib/rpc/sin
DoughnutWidgetComponent,
RangeChartWidgetComponent,
BarChartWithLabelsWidgetComponent,
SingleSwitchWidgetComponent
SingleSwitchWidgetComponent,
ActionButtonWidgetComponent
],
imports: [
CommonModule,
@ -162,7 +164,8 @@ import { SingleSwitchWidgetComponent } from '@home/components/widget/lib/rpc/sin
DoughnutWidgetComponent,
RangeChartWidgetComponent,
BarChartWithLabelsWidgetComponent,
SingleSwitchWidgetComponent
SingleSwitchWidgetComponent,
ActionButtonWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

2
ui-ngx/src/app/modules/home/components/widget/widget-container.component.html

@ -24,6 +24,7 @@
'tb-highlighted': isHighlighted(widget),
'tb-not-highlighted': isNotHighlighted(widget),
'mat-elevation-z4': widget.dropShadow,
'tb-overflow-visible': widgetComponent.widgetContext?.overflowVisible,
'tb-has-timewindow': widget.hasTimewindow,
'tb-edit': isEdit
}"
@ -88,6 +89,7 @@
<tb-widget #widgetComponent
[dashboardWidget]="widget"
[isEdit]="isEdit"
[isPreview]="isPreview"
[isMobile]="isMobile"
[widgetTitlePanel]="widgetTitlePanel">
</tb-widget>

7
ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss

@ -26,6 +26,13 @@
outline: none;
transition: all .2s ease-in-out;
&.tb-overflow-visible {
overflow: visible;
.tb-widget {
overflow: visible;
}
}
}
div.tb-widget {

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

@ -22,7 +22,6 @@ import {
ElementRef,
EventEmitter,
HostBinding,
Inject,
Input,
OnDestroy,
OnInit,
@ -36,10 +35,9 @@ import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-compon
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { SafeStyle } from '@angular/platform-browser';
import { guid, isNotEmptyStr } from '@core/utils';
import cssjs from '@core/css/css';
import { DOCUMENT } from '@angular/common';
import { isNotEmptyStr } from '@core/utils';
import { GridsterItemComponent } from 'angular-gridster2';
import { UtilsService } from '@core/services/utils.service';
export enum WidgetComponentActionType {
MOUSE_DOWN,
@ -86,6 +84,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
@Input()
isEdit: boolean;
@Input()
isPreview: boolean;
@Input()
isMobile: boolean;
@ -115,7 +116,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document) {
private utils: UtilsService) {
super(store);
}
@ -123,12 +124,8 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
this.widget.widgetContext.containerChangeDetector = this.cd;
const cssString = this.widget.widget.config.widgetCss;
if (isNotEmptyStr(cssString)) {
const cssParser = new cssjs();
cssParser.testMode = false;
this.cssClass = 'tb-widget-css-' + guid();
this.renderer.addClass(this.gridsterItem.el, this.cssClass);
cssParser.cssPreviewNamespace = this.cssClass;
cssParser.createStyleElement(this.cssClass, cssString);
this.cssClass =
this.utils.applyCssToElement(this.renderer, this.gridsterItem.el, 'tb-widget-css', cssString);
}
}
@ -138,10 +135,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, A
ngOnDestroy(): void {
if (this.cssClass) {
const el = this.document.getElementById(this.cssClass);
if (el) {
el.parentNode.removeChild(el);
}
this.utils.clearCssElement(this.renderer, this.cssClass);
}
}

1
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html

@ -25,6 +25,7 @@
[autofillHeight]="true"
[columns]="24"
[isEdit]="false"
[isPreview]="true"
[isMobileDisabled]="true"
[isEditActionEnabled]="false"
[isRemoveActionEnabled]="false">

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

@ -131,6 +131,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
@Input()
isEdit: boolean;
@Input()
isPreview: boolean;
@Input()
isMobile: boolean;
@ -231,6 +234,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext.store = this.store;
this.widgetContext.servicesMap = ServicesMap;
this.widgetContext.isEdit = this.isEdit;
this.widgetContext.isPreview = this.isPreview;
this.widgetContext.isMobile = this.isMobile;
this.widgetContext.toastTargetId = this.toastTargetId;
@ -252,6 +256,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
handleWidgetAction: this.handleWidgetAction.bind(this),
elementClick: this.elementClick.bind(this),
cardClick: this.cardClick.bind(this),
click: this.click.bind(this),
getActiveEntityInfo: this.getActiveEntityInfo.bind(this),
openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this),
openDashboardStateInPopover: this.openDashboardStateInPopover.bind(this)
@ -411,6 +416,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetType = this.widgetInfo.widgetTypeFunction;
this.typeParameters = this.widgetInfo.typeParameters;
this.widgetContext.embedTitlePanel = this.typeParameters.embedTitlePanel;
this.widgetContext.overflowVisible = this.typeParameters.overflowVisible;
if (!this.widgetType) {
this.widgetTypeInstance = {};
@ -1423,7 +1429,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
private cardClick($event: Event) {
const descriptors = this.getActionDescriptors('cardClick');
this.onClick($event, 'cardClick');
}
private click($event: Event) {
this.onClick($event, 'click');
}
private onClick($event: Event, sourceId: string) {
const descriptors = this.getActionDescriptors(sourceId);
if (descriptors.length) {
$event.stopPropagation();
const descriptor = descriptors[0];

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

@ -263,6 +263,7 @@ export class WidgetContext {
height: number;
$scope: IDynamicWidgetComponent;
isEdit: boolean;
isPreview: boolean;
isMobile: boolean;
toastTargetId: string;
@ -279,6 +280,7 @@ export class WidgetContext {
timeWindow?: WidgetTimewindow;
embedTitlePanel?: boolean;
overflowVisible?: boolean;
hideTitlePanel = false;

39
ui-ngx/src/app/shared/components/button/widget-button.component.html

@ -0,0 +1,39 @@
<!--
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.
-->
<button #widgetButton
mat-button
type="button"
class="tb-widget-button"
(click)="clicked.emit($event)"
(mousedown)="mousePressed = true"
(mouseup)="mousePressed = false"
(mouseleave)="mousePressed = false"
[disabled]="disabled"
[class]="'tb-'+appearance.type"
[class.tb-pressed]="mousePressed"
[class.tb-pressed-state]="pressed"
[class.tb-hover-state]="hovered"
[class.tb-active-state]="activated"
[class.tb-disabled-state]="disabled"
[style.border-radius]="borderRadius"
[style.pointer-events]="disableEvents ? 'none' : ''">
<div #widgetButtonContent class="tb-widget-button-content" *ngIf="appearance.showIcon || appearance.showLabel">
<tb-icon matButtonIcon *ngIf="appearance.showIcon" [style]="iconStyle">{{ appearance.icon }}</tb-icon>
<span *ngIf="appearance.showLabel" class="tb-widget-button-label">{{ appearance.label }}</span>
</div>
</button>

189
ui-ngx/src/app/shared/components/button/widget-button.component.scss

@ -0,0 +1,189 @@
/**
* 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.
*/
$defaultMainColor: #3F52DD;
$defaultBackgroundColor: #FFFFFF;
$defaultBoxShadowColor: rgba(0, 0, 0, 0.08);
$defaultDisabledBoxShadowColor: rgba(0, 0, 0, 0);
$defaultMainColorDisabled: rgba(0, 0, 0, 0.38);
$defaultBackgroundColorDisabled: rgba(0, 0, 0, 0.03);
$mainColorEnabled: var(--tb-widget-button-main-color-enabled, $defaultMainColor);
$backgroundColorEnabled: var(--tb-widget-button-background-color-enabled, $defaultBackgroundColor);
$boxShadowColorEnabled: var(--tb-widget-button-box-shadow-color-enabled, $defaultBoxShadowColor);
$mainColorHovered: var(--tb-widget-button-main-color-hovered, $defaultMainColor);
$backgroundColorHovered: var(--tb-widget-button-background-color-hovered, $defaultBackgroundColor);
$boxShadowColorHovered: var(--tb-widget-button-box-shadow-color-hovered, $defaultBoxShadowColor);
$mainColorHoveredFilled: var(--tb-widget-button-main-color-hovered-filled, #263BD7); // main.darken(6)
$mainColorPressed: var(--tb-widget-button-main-color-pressed, $defaultMainColor);
$backgroundColorPressed: var(--tb-widget-button-background-color-pressed, $defaultBackgroundColor);
$boxShadowColorPressed: var(--tb-widget-button-box-shadow-color-pressed, $defaultBoxShadowColor);
$mainColorPressedFilled: var(--tb-widget-button-main-color-pressed-filled, #2234BD); // main.darken(12)
$mainColorPressedRipple: var(--tb-widget-button-main-color-pressed-ripple, rgba(63, 82, 221, 0.1)); // Alpha(Main, 0.1)
$mainColorPressedRippleFilled: var(--tb-widget-button-main-color-pressed-ripple-filled, #1D2DA3); // main.darken(18)
$mainColorActivated: var(--tb-widget-button-main-color-activated, $defaultMainColor);
$backgroundColorActivated: var(--tb-widget-button-background-color-activated, $defaultBackgroundColor);
$boxShadowColorActivated: var(--tb-widget-button-box-shadow-color-activated, $defaultBoxShadowColor);
$mainColorActivatedFilled: var(--tb-widget-button-main-color-activated-filled, #2234BD); // main.darken(12)
$mainColorDisabled: var(--tb-widget-button-main-color-disabled, $defaultMainColorDisabled);
$backgroundColorDisabled: var(--tb-widget-button-background-color-disabled, $defaultBackgroundColorDisabled);
$boxShadowColorDisabled: var(--tb-widget-button-box-shadow-color-activated, $defaultBoxShadowColor);
@mixin _tb-widget-button-styles($main, $background, $boxShadow) {
color: $main;
background-color: $background;
box-shadow: 0 4px 8px 0 $boxShadow;
&.tb-outlined {
border: 1px solid $main;
}
&.tb-filled {
color: $background;
background-color: $main;
}
&.tb-underlined {
border-bottom: 2px solid $main;
}
&.tb-basic {
background-color: transparent;
}
}
.mat-mdc-button.mat-mdc-button-base.tb-widget-button {
width: 100%;
height: 100%;
padding: 8px 12px;
.mdc-button__label {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.tb-widget-button-content {
width: 100%;
display: flex;
flex-direction: row;
gap: 4px;
justify-content: center;
align-items: center;
.mat-icon {
margin: 0;
}
span.tb-widget-button-label {
line-height: normal;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.mat-mdc-button-persistent-ripple::before {
opacity: 0;
}
@include _tb-widget-button-styles($mainColorEnabled, $backgroundColorEnabled, $boxShadowColorEnabled);
&:not(:disabled):not(.tb-disabled-state) {
&:hover, &.tb-hover-state {
&:not(:active):not(.tb-active-state) {
&:not(.tb-filled) {
.mat-mdc-button-persistent-ripple::before {
opacity: 0.04;
background-color: $mainColorHovered;
}
}
&.tb-filled {
.mat-mdc-button-persistent-ripple::before {
opacity: 1;
background-color: $mainColorHoveredFilled;
}
}
@include _tb-widget-button-styles($mainColorHovered, $backgroundColorHovered, $boxShadowColorHovered);
}
}
&.tb-pressed-state {
&:not(.tb-filled) {
.mat-mdc-button-ripple {
background-color: $mainColorPressedRipple;
}
}
&.tb-filled {
.mat-mdc-button-ripple {
background-color: $mainColorPressedRippleFilled;
}
}
}
&.tb-pressed {
&:not(.tb-filled) {
.mat-ripple-element {
background-color: $mainColorPressedRipple;
}
}
&.tb-filled {
.mat-ripple-element {
background-color: $mainColorPressedRippleFilled;
}
}
}
&.tb-pressed, &.tb-pressed-state {
&:not(.tb-filled) {
.mat-mdc-button-persistent-ripple::before {
opacity: 0.12;
background-color: $mainColorPressed;
}
}
&.tb-filled {
.mat-mdc-button-persistent-ripple::before {
opacity: 1;
background-color: $mainColorPressedFilled;
}
}
@include _tb-widget-button-styles($mainColorPressed, $backgroundColorPressed, $boxShadowColorPressed);
}
&:active, &.tb-active-state {
&:not(.tb-pressed):not(.tb-pressed-state) {
&:not(.tb-filled) {
.mat-mdc-button-persistent-ripple::before {
opacity: 0.12;
background-color: $mainColorActivated;
}
}
&.tb-filled {
.mat-mdc-button-persistent-ripple::before {
opacity: 1;
background-color: $mainColorActivatedFilled;
}
}
@include _tb-widget-button-styles($mainColorActivated, $backgroundColorActivated, $boxShadowColorActivated);
}
}
}
&:disabled, &.tb-disabled-state {
&:not(.tb-filled) {
@include _tb-widget-button-styles($mainColorDisabled, $backgroundColorDisabled, $boxShadowColorDisabled);
}
&.tb-filled {
@include _tb-widget-button-styles($backgroundColorDisabled, $mainColorDisabled, $boxShadowColorDisabled);
}
}
}

178
ui-ngx/src/app/shared/components/button/widget-button.component.ts

@ -0,0 +1,178 @@
///
/// 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 {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
Renderer2,
SimpleChanges, ViewChild,
ViewEncapsulation
} from '@angular/core';
import {
generateWidgetButtonAppearanceCss,
widgetButtonDefaultAppearance
} from '@shared/components/button/widget-button.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { ComponentStyle, iconStyle } from '@shared/models/widget-settings.models';
import { UtilsService } from '@core/services/utils.service';
import { ResizeObserver } from '@juggle/resize-observer';
const initialButtonHeight = 60;
const horizontalLayoutPadding = 24;
const verticalLayoutPadding = 16;
@Component({
selector: 'tb-widget-button',
templateUrl: './widget-button.component.html',
styleUrls: ['./widget-button.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class WidgetButtonComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {
@ViewChild('widgetButton', {read: ElementRef})
widgetButton: ElementRef<HTMLElement>;
@ViewChild('widgetButtonContent', {static: false})
widgetButtonContent: ElementRef<HTMLElement>;
@Input()
appearance = widgetButtonDefaultAppearance;
@Input()
borderRadius = '4px';
@Input()
@coerceBoolean()
disabled = false;
@Input()
@coerceBoolean()
activated = false;
@Input()
@coerceBoolean()
hovered = false;
@Input()
@coerceBoolean()
pressed = false;
@Input()
@coerceBoolean()
disableEvents = false;
@Output()
clicked = new EventEmitter<MouseEvent>();
iconStyle: ComponentStyle = {};
mousePressed = false;
private buttonResize$: ResizeObserver;
private appearanceCssClass: string;
constructor(private renderer: Renderer2,
private elementRef: ElementRef,
private utils: UtilsService) {}
ngOnInit(): void {
this.updateAppearance();
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange) {
if (propName === 'appearance') {
this.updateAppearance();
}
}
}
}
ngAfterViewInit(): void {
this.updateAutoScale();
}
ngOnDestroy(): void {
if (this.buttonResize$) {
this.buttonResize$.disconnect();
}
this.clearAppearanceCss();
}
private updateAppearance(): void {
this.clearAppearanceCss();
if (this.appearance.showIcon) {
this.iconStyle = iconStyle(this.appearance.iconSize, this.appearance.iconSizeUnit);
}
const appearanceCss = generateWidgetButtonAppearanceCss(this.appearance);
this.appearanceCssClass = this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement,
'tb-widget-button', appearanceCss);
this.updateAutoScale();
}
private clearAppearanceCss(): void {
if (this.appearanceCssClass) {
this.utils.clearCssElement(this.renderer, this.appearanceCssClass, this.elementRef?.nativeElement);
this.appearanceCssClass = null;
}
}
private updateAutoScale() {
if (this.buttonResize$) {
this.buttonResize$.disconnect();
}
if (this.widgetButton && this.widgetButtonContent) {
if (this.appearance.autoScale) {
this.buttonResize$ = new ResizeObserver(() => {
this.onResize();
});
this.buttonResize$.observe(this.widgetButton.nativeElement);
this.onResize();
} else {
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'transform', 'none');
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'width', '100%');
}
}
}
private onResize() {
const height = this.widgetButton.nativeElement.getBoundingClientRect().height;
const buttonScale = height / initialButtonHeight;
const paddingScale = Math.min(buttonScale, 1);
const buttonWidth = this.widgetButton.nativeElement.getBoundingClientRect().width - (horizontalLayoutPadding * paddingScale);
const buttonHeight = this.widgetButton.nativeElement.getBoundingClientRect().height - (verticalLayoutPadding * paddingScale);
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'transform', `scale(1)`);
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'width', 'auto');
const contentWidth = this.widgetButtonContent.nativeElement.getBoundingClientRect().width;
const contentHeight = this.widgetButtonContent.nativeElement.getBoundingClientRect().height;
const maxScale = Math.max(1, buttonScale);
const scale = Math.min(Math.min(buttonWidth / contentWidth, buttonHeight / contentHeight), maxScale);
const targetWidth = buttonWidth / scale;
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'width', targetWidth + 'px');
this.renderer.setStyle(this.widgetButtonContent.nativeElement, 'transform', `scale(${scale})`);
}
}

259
ui-ngx/src/app/shared/components/button/widget-button.models.ts

@ -0,0 +1,259 @@
///
/// 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 { cssUnit } from '@shared/models/widget-settings.models';
import tinycolor from 'tinycolor2';
const defaultMainColor = '#3F52DD';
const defaultBackgroundColor = '#FFFFFF';
export const defaultMainColorDisabled = 'rgba(0, 0, 0, 0.38)';
export const defaultBackgroundColorDisabled = 'rgba(0, 0, 0, 0.03)';
const defaultBoxShadowColor = 'rgba(0, 0, 0, 0.08)';
const defaultDisabledBoxShadowColor = 'rgba(0, 0, 0, 0)';
export enum WidgetButtonType {
outlined = 'outlined',
filled = 'filled',
underlined = 'underlined',
basic = 'basic'
}
export const widgetButtonTypes = Object.keys(WidgetButtonType) as WidgetButtonType[];
export const widgetButtonTypeTranslations = new Map<WidgetButtonType, string>(
[
[WidgetButtonType.outlined, 'widgets.button.outlined'],
[WidgetButtonType.filled, 'widgets.button.filled'],
[WidgetButtonType.underlined, 'widgets.button.underlined'],
[WidgetButtonType.basic, 'widgets.button.basic']
]
);
export const widgetButtonTypeImages = new Map<WidgetButtonType, string>(
[
[WidgetButtonType.outlined, 'assets/widget/button/outlined.svg'],
[WidgetButtonType.filled, 'assets/widget/button/filled.svg'],
[WidgetButtonType.underlined, 'assets/widget/button/underlined.svg'],
[WidgetButtonType.basic, 'assets/widget/button/basic.svg']
]
);
export enum WidgetButtonState {
enabled = 'enabled',
hovered = 'hovered',
pressed = 'pressed',
activated = 'activated',
disabled = 'disabled'
}
export const widgetButtonStates = Object.keys(WidgetButtonState) as WidgetButtonState[];
export const widgetButtonStatesTranslations = new Map<WidgetButtonState, string>(
[
[WidgetButtonState.enabled, 'widgets.button-state.enabled'],
[WidgetButtonState.hovered, 'widgets.button-state.hovered'],
[WidgetButtonState.pressed, 'widgets.button-state.pressed'],
[WidgetButtonState.activated, 'widgets.button-state.activated'],
[WidgetButtonState.disabled, 'widgets.button-state.disabled']
]
);
export interface WidgetButtonCustomStyle {
overrideMainColor?: boolean;
mainColor?: string;
overrideBackgroundColor?: boolean;
backgroundColor?: string;
overrideDropShadow?: boolean;
dropShadow?: boolean;
}
export type WidgetButtonCustomStyles = Record<WidgetButtonState, WidgetButtonCustomStyle>;
export interface WidgetButtonAppearance {
type: WidgetButtonType;
autoScale: boolean;
showLabel: boolean;
label: string;
showIcon: boolean;
icon: string;
iconSize: number;
iconSizeUnit: cssUnit;
mainColor: string;
backgroundColor: string;
customStyle: WidgetButtonCustomStyles;
}
export const widgetButtonDefaultAppearance: WidgetButtonAppearance = {
type: WidgetButtonType.outlined,
autoScale: true,
showLabel: true,
label: 'Button',
showIcon: true,
icon: 'home',
iconSize: 24,
iconSizeUnit: 'px',
mainColor: defaultMainColor,
backgroundColor: defaultBackgroundColor,
customStyle: {
enabled: null,
hovered: null,
pressed: null,
activated: null,
disabled: null
}
};
const mainColorVarPrefix = '--tb-widget-button-main-color-';
const backgroundColorVarPrefix = '--tb-widget-button-background-color-';
const boxShadowColorVarPrefix = '--tb-widget-button-box-shadow-color-';
abstract class ButtonStateCssGenerator {
constructor() {}
public generateStateCss(appearance: WidgetButtonAppearance): string {
let mainColor = this.getMainColor(appearance);
let backgroundColor = this.getBackgroundColor(appearance);
const shadowEnabledByDefault = appearance.type !== WidgetButtonType.basic;
let shadowColor = shadowEnabledByDefault ? defaultBoxShadowColor : defaultDisabledBoxShadowColor;
const stateCustomStyle = appearance.customStyle[this.state];
if (stateCustomStyle?.overrideMainColor && stateCustomStyle?.mainColor) {
mainColor = stateCustomStyle.mainColor;
}
if (stateCustomStyle?.overrideBackgroundColor && stateCustomStyle?.backgroundColor) {
backgroundColor = stateCustomStyle.backgroundColor;
}
if (stateCustomStyle?.overrideDropShadow) {
shadowColor = !!stateCustomStyle.dropShadow ? defaultBoxShadowColor : defaultDisabledBoxShadowColor;
}
let css = `${mainColorVarPrefix}${this.state}: ${mainColor};\n`+
`${backgroundColorVarPrefix}${this.state}: ${backgroundColor};\n`+
`${boxShadowColorVarPrefix}${this.state}: ${shadowColor};`;
const additionalCss = this.generateAdditionalStateCss(mainColor, backgroundColor);
if (additionalCss) {
css += `\n${additionalCss}`;
}
return css;
}
protected abstract get state(): WidgetButtonState;
protected getMainColor(appearance: WidgetButtonAppearance): string {
return appearance.mainColor || defaultMainColor;
}
protected getBackgroundColor(appearance: WidgetButtonAppearance): string {
return appearance.backgroundColor || defaultBackgroundColor;
}
protected generateAdditionalStateCss(_mainColor: string, _backgroundColor: string): string {
return null;
}
}
class EnabledButtonStateCssGenerator extends ButtonStateCssGenerator {
protected get state(): WidgetButtonState {
return WidgetButtonState.enabled;
}
}
class HoveredButtonStateCssGenerator extends ButtonStateCssGenerator {
protected get state(): WidgetButtonState {
return WidgetButtonState.hovered;
}
protected generateAdditionalStateCss(mainColor: string): string {
const mainColorHoveredFilled = darkenColor(mainColor, 6);
return `--tb-widget-button-main-color-hovered-filled: ${mainColorHoveredFilled};`;
}
}
class PressedButtonStateCssGenerator extends ButtonStateCssGenerator {
protected get state(): WidgetButtonState {
return WidgetButtonState.pressed;
}
protected generateAdditionalStateCss(mainColor: string): string {
const mainColorPressedFilled = darkenColor(mainColor, 12);
const mainColorInstance = tinycolor(mainColor);
const mainColorPressedRipple = mainColorInstance.setAlpha(mainColorInstance.getAlpha() * 0.1).toRgbString();
const mainColorPressedRippleFilled = darkenColor(mainColor, 18);
return `--tb-widget-button-main-color-pressed-filled: ${mainColorPressedFilled};\n`+
`--tb-widget-button-main-color-pressed-ripple: ${mainColorPressedRipple};\n`+
`--tb-widget-button-main-color-pressed-ripple-filled: ${mainColorPressedRippleFilled};`;
}
}
class ActivatedButtonStateCssGenerator extends ButtonStateCssGenerator {
protected get state(): WidgetButtonState {
return WidgetButtonState.activated;
}
protected generateAdditionalStateCss(mainColor: string): string {
const mainColorActivatedFilled = darkenColor(mainColor, 12);
return `--tb-widget-button-main-color-activated-filled: ${mainColorActivatedFilled};`;
}
}
class DisabledButtonStateCssGenerator extends ButtonStateCssGenerator {
protected get state(): WidgetButtonState {
return WidgetButtonState.disabled;
}
protected getMainColor(): string {
return defaultMainColorDisabled;
}
protected getBackgroundColor(): string {
return defaultBackgroundColorDisabled;
}
}
const buttonStateCssGeneratorsMap = new Map<WidgetButtonState, ButtonStateCssGenerator>(
[
[WidgetButtonState.enabled, new EnabledButtonStateCssGenerator()],
[WidgetButtonState.hovered, new HoveredButtonStateCssGenerator()],
[WidgetButtonState.pressed, new PressedButtonStateCssGenerator()],
[WidgetButtonState.activated, new ActivatedButtonStateCssGenerator()],
[WidgetButtonState.disabled, new DisabledButtonStateCssGenerator()]
]
);
const widgetButtonCssSelector = '.mat-mdc-button.mat-mdc-button-base.tb-widget-button';
export const generateWidgetButtonAppearanceCss = (appearance: WidgetButtonAppearance): string => {
let statesCss = '';
for (const state of widgetButtonStates) {
const generator = buttonStateCssGeneratorsMap.get(state);
statesCss += `\n${generator.generateStateCss(appearance)}`;
}
return `${widgetButtonCssSelector} {\n`+
`${statesCss}\n`+
`}`;
};
const darkenColor = (inputColor: string, amount: number): string => {
const input = tinycolor(inputColor);
return input.darken(amount).toRgbString();
};

19
ui-ngx/src/app/shared/models/action-widget-settings.models.ts

@ -15,6 +15,7 @@
///
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { widgetType } from '@shared/models/widget.models';
export enum GetValueAction {
DO_NOTHING = 'DO_NOTHING',
@ -25,6 +26,14 @@ export enum GetValueAction {
export const getValueActions = Object.keys(GetValueAction) as GetValueAction[];
export const getValueActionsByWidgetType = (type: widgetType): GetValueAction[] => {
if (type !== widgetType.rpc) {
return getValueActions.filter(action => action !== GetValueAction.EXECUTE_RPC);
} else {
return getValueActions;
}
};
export const getValueActionTranslations = new Map<GetValueAction, string>(
[
[GetValueAction.DO_NOTHING, 'widgets.value-action.do-nothing'],
@ -75,7 +84,7 @@ export interface ValueActionSettings {
export interface GetValueSettings<V> extends ValueActionSettings {
action: GetValueAction;
defaultValue: V;
executeRpc: RpcSettings;
executeRpc?: RpcSettings;
getAttribute: GetAttributeValueSettings;
getTimeSeries: GetTelemetryValueSettings;
dataToValue: DataToValueSettings;
@ -89,6 +98,14 @@ export enum SetValueAction {
export const setValueActions = Object.keys(SetValueAction) as SetValueAction[];
export const setValueActionsByWidgetType = (type: widgetType): SetValueAction[] => {
if (type !== widgetType.rpc) {
return setValueActions.filter(action => action !== SetValueAction.EXECUTE_RPC);
} else {
return setValueActions;
}
};
export const setValueActionTranslations = new Map<SetValueAction, string>(
[
[SetValueAction.EXECUTE_RPC, 'widgets.value-action.execute-rpc'],

27
ui-ngx/src/app/shared/models/widget-settings.models.ts

@ -15,7 +15,14 @@
///
import { isDefinedAndNotNull, isNumber, isNumeric, isUndefinedOrNull, parseFunction } from '@core/utils';
import { DataEntry, DataKey, Datasource, DatasourceData } from '@shared/models/widget.models';
import {
DataEntry,
DataKey,
Datasource,
DatasourceData,
DatasourceType,
TargetDevice, TargetDeviceType
} from '@shared/models/widget.models';
import { Injector } from '@angular/core';
import { DatePipe } from '@angular/common';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
@ -600,6 +607,24 @@ export const updateDataKeyByLabel = (datasources: Datasource[], dataKey: DataKey
}
};
export const getTargetDeviceFromDatasources = (datasources?: Datasource[]): TargetDevice => {
if (datasources && datasources.length) {
const datasource = datasources[0];
if (datasource?.type === DatasourceType.device) {
return {
type: TargetDeviceType.device,
deviceId: datasource?.deviceId
};
} else if (datasource?.type === DatasourceType.entity) {
return {
type: TargetDeviceType.entity,
entityAliasId: datasource?.entityAliasId
};
}
}
return null;
};
export const getAlarmFilterConfig = (datasources?: Datasource[]): AlarmFilterConfig => {
if (datasources && datasources.length) {
const config = datasources[0].alarmFilterConfig;

26
ui-ngx/src/app/shared/models/widget.models.ts

@ -180,6 +180,7 @@ export interface WidgetTypeParameters {
previewWidth?: string;
previewHeight?: string;
embedTitlePanel?: boolean;
overflowVisible?: boolean;
hideDataSettings?: boolean;
defaultDataKeysFunction?: (configComponent: any, configData: any) => DataKey[];
defaultLatestDataKeysFunction?: (configComponent: any, configData: any) => DataKey[];
@ -410,6 +411,23 @@ export interface Datasource {
[key: string]: any;
}
export const datasourceValid = (datasource: Datasource): boolean => {
const type: DatasourceType = datasource?.type;
if (type) {
switch (type) {
case DatasourceType.function:
case DatasourceType.alarmCount:
return true;
case DatasourceType.device:
return !!datasource.deviceId;
case DatasourceType.entity:
case DatasourceType.entityCount:
return !!datasource.entityAliasId;
}
}
return false;
};
export enum TargetDeviceType {
device = 'device',
entity = 'entity'
@ -675,6 +693,14 @@ export const actionDescriptorToAction = (descriptor: WidgetActionDescriptor): Wi
return result;
};
export const defaultWidgetAction = (setEntityId = true): WidgetAction => ({
type: WidgetActionType.updateDashboardState,
targetDashboardStateId: null,
openRightLayout: false,
setEntityId,
stateEntityParamName: null
});
export interface WidgetComparisonSettings {
comparisonEnabled?: boolean;
timeForComparison?: moment_.unitOfTime.DurationConstructor;

7
ui-ngx/src/app/shared/shared.module.ts

@ -217,6 +217,7 @@ import { MultipleGalleryImageInputComponent } from '@shared/components/image/mul
import { EmbedImageDialogComponent } from '@shared/components/image/embed-image-dialog.component';
import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-dialog.component';
import { RuleChainSelectPanelComponent } from '@shared/components/rule-chain/rule-chain-select-panel.component';
import { WidgetButtonComponent } from '@shared/components/button/widget-button.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -414,7 +415,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GalleryImageInputComponent,
MultipleGalleryImageInputComponent,
EmbedImageDialogComponent,
ImageGalleryDialogComponent
ImageGalleryDialogComponent,
WidgetButtonComponent
],
imports: [
CommonModule,
@ -666,7 +668,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GalleryImageInputComponent,
MultipleGalleryImageInputComponent,
EmbedImageDialogComponent,
ImageGalleryDialogComponent
ImageGalleryDialogComponent,
WidgetButtonComponent
]
})
export class SharedModule { }

47
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -5182,6 +5182,42 @@
"invalid-widget-file-error": "Unable to import widget: Invalid widget data structure."
},
"widgets": {
"action-button": {
"behavior": "Behavior",
"on-click": "On click",
"on-click-hint": "Action performed when the button is clicked."
},
"button": {
"layout": "Layout",
"outlined": "Outlined",
"filled": "Filled",
"underlined": "Underlined",
"basic": "Basic",
"auto-scale": "Auto scale",
"label": "Label",
"icon": "Icon",
"color-palette": "Color palette",
"main": "Main",
"background": "Background",
"custom-styles": "Custom styles",
"clear-style": "Clear style",
"shadow": "Shadow",
"enabled": "Enabled",
"disabled": "Disabled",
"preview": "Preview",
"copy-style-from": "Copy style from"
},
"button-state": {
"activated-state": "Activated state",
"activated-state-hint": "Condition under which the button is active.",
"disabled-state": "Disabled state",
"disabled-state-hint": "Condition under which the button is disabled.",
"enabled": "Enabled",
"hovered": "Hovered",
"pressed": "Pressed",
"activated": "Activated",
"disabled": "Disabled"
},
"background": {
"background": "Background",
"background-settings": "Background settings",
@ -6483,7 +6519,7 @@
"source-entity-alias": "Source entity alias",
"source-entity-attribute": "Source entity attribute"
},
"value-action": {
"rpc-state": {
"initial-state": "Initial state",
"initial-state-hint": "Action to get the initial value of the component.",
"turn-on": "Turn 'On'",
@ -6491,7 +6527,9 @@
"turn-off": "Turn 'Off'",
"turn-off-hint": "Action performed to turn OFF the component.",
"on": "On",
"off": "Off",
"off": "Off"
},
"value-action": {
"do-nothing": "Do nothing",
"execute-rpc": "Execute RPC",
"get-attribute": "Get attribute",
@ -6530,7 +6568,7 @@
"converter-function": "Function",
"converter-constant": "Constant",
"parse-value-function": "Parse value function",
"on-when-result-is": "'On' when result is",
"state-when-result-is": "'{{state}}' when result is",
"parameters": "Parameters",
"convert-value-function": "Convert value function",
"error": {
@ -6785,7 +6823,8 @@
"element-click": "On HTML element click",
"pie-slice-click": "On slice click",
"row-double-click": "On row double click",
"card-click": "On card click"
"card-click": "On card click",
"click": "On click"
}
},
"paginator" : {

17
ui-ngx/src/assets/widget/button/basic.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

19
ui-ngx/src/assets/widget/button/filled.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

20
ui-ngx/src/assets/widget/button/outlined.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

23
ui-ngx/src/assets/widget/button/underlined.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

8
ui-ngx/src/form.scss

@ -607,6 +607,14 @@
}
}
button.mat-mdc-button-base.tb-nowrap {
.mdc-button__label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.mat-mdc-chip-listbox.center-stretch {
.mat-mdc-standard-chip {
flex: 1;

Loading…
Cancel
Save