Browse Source

Merge pull request #10132 from thingsboard/feature/action-button

Action button widget
pull/10138/head
Igor Kulikov 3 years ago
committed by GitHub
parent
commit
2fab6dc5c6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  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. 2
      application/src/main/data/json/system/widget_types/single_switch.json
  4. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  5. 26
      ui-ngx/src/app/core/services/utils.service.ts
  6. 16
      ui-ngx/src/app/modules/common/modules-map.ts
  7. 1
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  8. 3
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  9. 11
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  10. 12
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  11. 77
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.html
  12. 139
      ui-ngx/src/app/modules/home/components/widget/config/basic/button/action-button-basic-config.component.ts
  13. 29
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.html
  14. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/single-switch-basic-config.component.ts
  15. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.ts
  16. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  17. 30
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  18. 5
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  19. 17
      ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
  20. 14
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  21. 42
      ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.models.ts
  22. 46
      ui-ngx/src/app/modules/home/components/widget/lib/action/action-widget.scss
  23. 35
      ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.html
  24. 28
      ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.scss
  25. 100
      ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.component.ts
  26. 63
      ui-ngx/src/app/modules/home/components/widget/lib/button/action-button-widget.models.ts
  27. 14
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.html
  28. 38
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.scss
  29. 80
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.ts
  30. 21
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.models.ts
  31. 51
      ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.html
  32. 70
      ui-ngx/src/app/modules/home/components/widget/lib/settings/button/action-button-widget-settings.component.ts
  33. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/action-settings-button.component.html
  34. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/action-settings-button.scss
  35. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/action-settings-panel.component.scss
  36. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.html
  37. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.scss
  38. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component.ts
  39. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.html
  40. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.scss
  41. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component.ts
  42. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-action.models.ts
  43. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-sample-css.raw
  44. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-sample-html.raw
  45. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/custom-sample-js.raw
  46. 24
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.html
  47. 27
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component.ts
  48. 29
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/get-value-action-settings.component.ts
  49. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html
  50. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts
  51. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts
  52. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.html
  53. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component.ts
  54. 14
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/set-value-action-settings.component.ts
  55. 42
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html
  56. 88
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts
  57. 136
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts
  58. 0
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html
  59. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts
  60. 97
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.html
  61. 141
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-appearance.component.ts
  62. 93
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.html
  63. 68
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.scss
  64. 171
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style-panel.component.ts
  65. 44
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.html
  66. 46
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.scss
  67. 158
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-custom-style.component.ts
  68. 47
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  69. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts
  70. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/round-switch-widget-settings.component.ts
  71. 29
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.html
  72. 16
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/single-switch-widget-settings.component.ts
  73. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts
  74. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/switch-control-widget-settings.component.ts
  75. 20
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  76. 3
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  77. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  78. 2
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  79. 7
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  80. 24
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  81. 1
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
  82. 16
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  83. 2
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  84. 39
      ui-ngx/src/app/shared/components/button/widget-button.component.html
  85. 189
      ui-ngx/src/app/shared/components/button/widget-button.component.scss
  86. 178
      ui-ngx/src/app/shared/components/button/widget-button.component.ts
  87. 271
      ui-ngx/src/app/shared/components/button/widget-button.models.ts
  88. 2
      ui-ngx/src/app/shared/components/color-input.component.ts
  89. 2
      ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss
  90. 37
      ui-ngx/src/app/shared/models/action-widget-settings.models.ts
  91. 27
      ui-ngx/src/app/shared/models/widget-settings.models.ts
  92. 26
      ui-ngx/src/app/shared/models/widget.models.ts
  93. 7
      ui-ngx/src/app/shared/shared.module.ts
  94. 50
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  95. 17
      ui-ngx/src/assets/widget/button/basic.svg
  96. 19
      ui-ngx/src/assets/widget/button/filled.svg
  97. 20
      ui-ngx/src/assets/widget/button/outlined.svg
  98. 23
      ui-ngx/src/assets/widget/button/underlined.svg
  99. 8
      ui-ngx/src/form.scss

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

2
application/src/main/data/json/system/widget_types/single_switch.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; entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void;
elementClick: ($event: Event) => void; elementClick: ($event: Event) => void;
cardClick: ($event: Event) => void; cardClick: ($event: Event) => void;
click: ($event: Event) => void;
getActiveEntityInfo: () => SubscriptionEntityInfo; getActiveEntityInfo: () => SubscriptionEntityInfo;
openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string,
hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void; 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 // eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../src/typings/rawloader.typings.d.ts" /> /// <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 { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models'; import { ExceptionData } from '@app/shared/models/error.models';
import { import {
@ -55,8 +55,9 @@ import {
TelemetryType TelemetryType
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; 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 { entityTypeTranslations } from '@shared/models/entity-type.models';
import cssjs from '@core/css/css';
const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g'); const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g');
@ -116,6 +117,7 @@ export class UtilsService {
defaultAlarmDataKeys: Array<DataKey> = []; defaultAlarmDataKeys: Array<DataKey> = [];
constructor(@Inject(WINDOW) private window: Window, constructor(@Inject(WINDOW) private window: Window,
@Inject(DOCUMENT) private document: Document,
private zone: NgZone, private zone: NgZone,
private datePipe: DatePipe, private datePipe: DatePipe,
private translate: TranslateService) { private translate: TranslateService) {
@ -502,4 +504,24 @@ export class UtilsService {
return base64toObj(b64Encoded); 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. /// limitations under the License.
/// ///
/* eslint-disable max-len */
import * as AngularAnimations from '@angular/animations'; import * as AngularAnimations from '@angular/animations';
import * as AngularCore from '@angular/core'; import * as AngularCore from '@angular/core';
import * as AngularCommon from '@angular/common'; 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 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 ManageWidgetActionsComponent from '@home/components/widget/action/manage-widget-actions.component';
import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.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 CustomActionPrettyResourcesTabsComponent from '@home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component';
import * as CustomActionPrettyEditorComponent from '@home/components/widget/config/action/custom-action-pretty-editor.component'; import * as CustomActionPrettyEditorComponent from '@home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component';
import * as MobileActionEditorComponent from '@home/components/widget/config/action/mobile-action-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 CustomDialogService from '@home/components/widget/dialog/custom-dialog.service';
import * as CustomDialogContainerComponent from '@home/components/widget/dialog/custom-dialog-container.component'; import * as CustomDialogContainerComponent from '@home/components/widget/dialog/custom-dialog-container.component';
import * as ImportDialogComponent from '@shared/import-export/import-dialog.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 TenantProfileComponent from '@home/components/profile/tenant-profile.component';
import * as TenantProfileDialogComponent from '@home/components/profile/tenant-profile-dialog.component'; import * as TenantProfileDialogComponent from '@home/components/profile/tenant-profile-dialog.component';
import * as TenantProfileDataComponent from '@home/components/profile/tenant-profile-data.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 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 DeviceProfileConfigurationComponent from '@home/components/profile/device/device-profile-configuration.component';
import * as DeviceProfileComponent from '@home/components/profile/device-profile.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 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 EditAlarmDetailsDialogComponent from '@home/components/profile/alarm/edit-alarm-details-dialog.component';
import * as AlarmRuleConditionDialogComponent from '@home/components/profile/alarm/alarm-rule-condition-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 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 TenantProfileConfigurationComponent from '@home/components/profile/tenant/tenant-profile-configuration.component';
import * as SmsProviderConfigurationComponent from '@home/components/sms/sms-provider-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/lib/settings/common/legend-config.component': LegendConfigComponent,
'@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent, '@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent,
'@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent, '@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent,
'@home/components/widget/config/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent, '@home/components/widget/lib/settings/common/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent,
'@home/components/widget/config/action/custom-action-pretty-editor.component': CustomActionPrettyEditorComponent, '@home/components/widget/lib/settings/common/action/custom-action-pretty-editor.component': CustomActionPrettyEditorComponent,
'@home/components/widget/config/action/mobile-action-editor.component': MobileActionEditorComponent, '@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.service': CustomDialogService,
'@home/components/widget/dialog/custom-dialog-container.component': CustomDialogContainerComponent, '@home/components/widget/dialog/custom-dialog-container.component': CustomDialogContainerComponent,
'@home/components/attribute/add-widget-to-dashboard-dialog.component': AddWidgetToDashboardDialogComponent, '@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" [dashboardStyle]="dashboardStyle"
[backgroundImage]="backgroundImage" [backgroundImage]="backgroundImage"
[isEdit]="isEdit" [isEdit]="isEdit"
[isPreview]="isPreview"
[isMobile]="isMobileSize" [isMobile]="isMobileSize"
[isEditActionEnabled]="isEditActionEnabled" [isEditActionEnabled]="isEditActionEnabled"
[isExportActionEnabled]="isExportActionEnabled" [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() @Input()
isEdit: boolean; isEdit: boolean;
@Input()
isPreview: boolean;
@Input() @Input()
autofillHeight: boolean; 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'; } from '@home/components/widget/action/manage-widget-actions.component.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { import {
actionDescriptorToAction, actionDescriptorToAction, defaultWidgetAction,
WidgetActionSource, WidgetActionSource,
WidgetActionType,
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { takeUntil } from 'rxjs/operators'; 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'; import { WidgetService } from '@core/http/widget.service';
export interface WidgetActionDialogData { export interface WidgetActionDialogData {
@ -92,11 +91,7 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
id: this.utils.guid(), id: this.utils.guid(),
name: '', name: '',
icon: 'more_horiz', icon: 'more_horiz',
type: WidgetActionType.updateDashboardState, ...defaultWidgetAction(data.widgetType !== widgetType.static)
targetDashboardStateId: null,
openRightLayout: false,
setEntityId: data.widgetType !== widgetType.static,
stateEntityParamName: null
}; };
} else { } else {
this.action = this.data.action; 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 { import {
SingleSwitchBasicConfigComponent SingleSwitchBasicConfigComponent
} from '@home/components/widget/config/basic/rpc/single-switch-basic-config.component'; } 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({ @NgModule({
declarations: [ declarations: [
@ -123,7 +126,8 @@ import {
DoughnutBasicConfigComponent, DoughnutBasicConfigComponent,
RangeChartBasicConfigComponent, RangeChartBasicConfigComponent,
BarChartWithLabelsBasicConfigComponent, BarChartWithLabelsBasicConfigComponent,
SingleSwitchBasicConfigComponent SingleSwitchBasicConfigComponent,
ActionButtonBasicConfigComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -157,7 +161,8 @@ import {
DoughnutBasicConfigComponent, DoughnutBasicConfigComponent,
RangeChartBasicConfigComponent, RangeChartBasicConfigComponent,
BarChartWithLabelsBasicConfigComponent, BarChartWithLabelsBasicConfigComponent,
SingleSwitchBasicConfigComponent SingleSwitchBasicConfigComponent,
ActionButtonBasicConfigComponent
] ]
}) })
export class BasicWidgetConfigModule { export class BasicWidgetConfigModule {
@ -185,5 +190,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-doughnut-basic-config': DoughnutBasicConfigComponent, 'tb-doughnut-basic-config': DoughnutBasicConfigComponent,
'tb-range-chart-basic-config': RangeChartBasicConfigComponent, 'tb-range-chart-basic-config': RangeChartBasicConfigComponent,
'tb-bar-chart-with-labels-basic-config': BarChartWithLabelsBasicConfigComponent, '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};
}
}
}

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

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

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

@ -70,6 +70,7 @@ export class SingleSwitchBasicConfigComponent extends BasicWidgetConfigComponent
initialState: [settings.initialState, []], initialState: [settings.initialState, []],
onUpdateState: [settings.onUpdateState, []], onUpdateState: [settings.onUpdateState, []],
offUpdateState: [settings.offUpdateState, []], offUpdateState: [settings.offUpdateState, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []], layout: [settings.layout, []],
autoScale: [settings.autoScale, []], autoScale: [settings.autoScale, []],
@ -120,6 +121,7 @@ export class SingleSwitchBasicConfigComponent extends BasicWidgetConfigComponent
this.widgetConfig.config.settings.initialState = config.initialState; this.widgetConfig.config.settings.initialState = config.initialState;
this.widgetConfig.config.settings.onUpdateState = config.onUpdateState; this.widgetConfig.config.settings.onUpdateState = config.onUpdateState;
this.widgetConfig.config.settings.offUpdateState = config.offUpdateState; this.widgetConfig.config.settings.offUpdateState = config.offUpdateState;
this.widgetConfig.config.settings.disabledState = config.disabledState;
this.widgetConfig.config.settings.layout = config.layout; this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.autoScale = config.autoScale; this.widgetConfig.config.settings.autoScale = config.autoScale;

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

@ -521,7 +521,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, OnChange
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer, const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null, this.viewContainerRef, ColorPickerPanelComponent, ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
{ {
color: key.color color: key.color
}, },

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

@ -42,14 +42,14 @@
style="height: 56px; margin-bottom: 22px;" style="height: 56px; margin-bottom: 22px;"
formControlName="alarmFilterConfig"></tb-alarm-filter-config> formControlName="alarmFilterConfig"></tb-alarm-filter-config>
<tb-entity-autocomplete *ngIf="datasourceFormGroup.get('type').value === datasourceType.device" <tb-entity-autocomplete *ngIf="datasourceFormGroup.get('type').value === datasourceType.device"
required [required]="!datasourcesOptional"
[entityType]="entityType.DEVICE" [entityType]="entityType.DEVICE"
formControlName="deviceId"> formControlName="deviceId">
</tb-entity-autocomplete> </tb-entity-autocomplete>
<tb-entity-alias-select <tb-entity-alias-select
*ngIf="datasourceFormGroup.get('type').value !== datasourceType.device && datasourceFormGroup.get('type').value !== datasourceType.alarmCount" *ngIf="datasourceFormGroup.get('type').value !== datasourceType.device && datasourceFormGroup.get('type').value !== datasourceType.alarmCount"
[showLabel]="true" [showLabel]="true"
[tbRequired]="true" [tbRequired]="!datasourcesOptional"
[aliasController]="aliasController" [aliasController]="aliasController"
formControlName="entityAliasId" formControlName="entityAliasId"
[callbacks]="entityAliasSelectCallbacks"> [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; return this.widgetConfigComponent.modelValue?.typeParameters?.dataKeysOptional;
} }
public get datasourcesOptional(): boolean {
return this.widgetConfigComponent.modelValue?.typeParameters?.datasourcesOptional;
}
public get maxDataKeys(): number { public get maxDataKeys(): number {
return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys; return this.widgetConfigComponent.modelValue?.typeParameters?.maxDataKeys;
} }
@ -276,18 +280,20 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
} }
private updateValidators() { private updateValidators() {
const type: DatasourceType = this.datasourceFormGroup.get('type').value; if (!this.datasourcesOptional) {
this.datasourceFormGroup.get('deviceId').setValidators( const type: DatasourceType = this.datasourceFormGroup.get('type').value;
type === DatasourceType.device ? [Validators.required] : [] this.datasourceFormGroup.get('deviceId').setValidators(
); type === DatasourceType.device ? [Validators.required] : []
this.datasourceFormGroup.get('entityAliasId').setValidators( );
(type === DatasourceType.entity || type === DatasourceType.entityCount) ? [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] : []); const newDataKeysRequired = !this.isDataKeysOptional(type);
this.datasourceFormGroup.get('deviceId').updateValueAndValidity({emitEvent: false}); this.datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []);
this.datasourceFormGroup.get('entityAliasId').updateValueAndValidity({emitEvent: false}); this.datasourceFormGroup.get('deviceId').updateValueAndValidity({emitEvent: false});
this.datasourceFormGroup.get('dataKeys').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 { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { import {
Datasource, Datasource,
DatasourceType, DatasourceType, datasourceValid,
JsonSettingsSchema, JsonSettingsSchema,
WidgetConfigMode, WidgetConfigMode,
widgetType widgetType
@ -317,6 +317,9 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
} }
private datasourcesUpdated(datasources: Datasource[]) { private datasourcesUpdated(datasources: Datasource[]) {
if (this.datasourcesOptional) {
datasources = datasources ? datasources.filter(d => datasourceValid(d)) : [];
}
this.propagateChange(datasources); 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 { TimewindowStyleComponent } from '@home/components/widget/config/timewindow-style.component';
import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component'; import { TimewindowStylePanelComponent } from '@home/components/widget/config/timewindow-style-panel.component';
import { TargetDeviceComponent } from '@home/components/widget/config/target-device.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({ @NgModule({
declarations: declarations:
@ -55,11 +50,7 @@ import { MobileActionEditorComponent } from '@home/components/widget/config/acti
TimewindowStyleComponent, TimewindowStyleComponent,
TimewindowStylePanelComponent, TimewindowStylePanelComponent,
TimewindowConfigPanelComponent, TimewindowConfigPanelComponent,
WidgetSettingsComponent, WidgetSettingsComponent
WidgetActionComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -82,11 +73,7 @@ import { MobileActionEditorComponent } from '@home/components/widget/config/acti
TimewindowStylePanelComponent, TimewindowStylePanelComponent,
TimewindowConfigPanelComponent, TimewindowConfigPanelComponent,
WidgetSettingsComponent, WidgetSettingsComponent,
WidgetSettingsCommonModule, WidgetSettingsCommonModule
WidgetActionComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent
] ]
}) })
export class WidgetConfigComponentsModule { } 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; 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 { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
@ -63,6 +63,18 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
return this.widgetConfigComponent.aliasController; 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>(); widgetConfigChangedEmitter = new EventEmitter<WidgetConfigComponentData>();
widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable(); widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable();

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

@ -31,7 +31,6 @@ import {
DataToValueSettings, DataToValueSettings,
DataToValueType, DataToValueType,
GetAttributeValueSettings, GetAttributeValueSettings,
GetTelemetryValueSettings,
GetValueAction, GetValueAction,
GetValueSettings, GetValueSettings,
RpcSettings, RpcSettings,
@ -405,7 +404,7 @@ export class ExecuteRpcValueGetter<V> extends ValueGetter<V> {
} }
} }
export abstract class TelemetryValueGetter<V, S extends GetTelemetryValueSettings> extends ValueGetter<V> { export abstract class TelemetryValueGetter<V, S extends TelemetryValueSettings> extends ValueGetter<V> {
protected targetEntityId: EntityId; protected targetEntityId: EntityId;
private telemetrySubscriber: TelemetrySubscriber; private telemetrySubscriber: TelemetrySubscriber;
@ -428,11 +427,7 @@ export abstract class TelemetryValueGetter<V, S extends GetTelemetryValueSetting
if (err) { if (err) {
return throwError(() => err); return throwError(() => err);
} }
if (this.getTelemetryValueSettings().subscribeForUpdates) { return this.subscribeForTelemetryValue();
return this.subscribeForTelemetryValue();
} else {
return this.doGetTelemetryValue();
}
} else { } else {
return of(null); return of(null);
} }
@ -464,8 +459,6 @@ export abstract class TelemetryValueGetter<V, S extends GetTelemetryValueSetting
protected abstract getTelemetryValueSettings(): S; protected abstract getTelemetryValueSettings(): S;
protected abstract doGetTelemetryValue(): Observable<V>;
destroy() { destroy() {
if (this.telemetrySubscriber) { if (this.telemetrySubscriber) {
this.telemetrySubscriber.unsubscribe(); this.telemetrySubscriber.unsubscribe();
@ -492,17 +485,9 @@ export class AttributeValueGetter<V> extends TelemetryValueGetter<V, GetAttribut
return this.getTelemetryValueSettings().scope; return this.getTelemetryValueSettings().scope;
} }
protected doGetTelemetryValue(): Observable<V> {
const getAttributeValueSettings = this.getTelemetryValueSettings();
return this.ctx.attributeService.getEntityAttributes(this.targetEntityId,
getAttributeValueSettings.scope, [getAttributeValueSettings.key], {ignoreLoading: true, ignoreErrors: true}).pipe(
map((data) => data.find(attr => attr.key === getAttributeValueSettings.key)?.value)
);
}
} }
export class TimeSeriesValueGetter<V> extends TelemetryValueGetter<V, GetTelemetryValueSettings> { export class TimeSeriesValueGetter<V> extends TelemetryValueGetter<V, TelemetryValueSettings> {
constructor(protected ctx: WidgetContext, constructor(protected ctx: WidgetContext,
protected settings: GetValueSettings<V>, protected settings: GetValueSettings<V>,
@ -511,28 +496,9 @@ export class TimeSeriesValueGetter<V> extends TelemetryValueGetter<V, GetTelemet
super(ctx, settings, valueType, valueObserver); super(ctx, settings, valueType, valueObserver);
} }
protected getTelemetryValueSettings(): GetTelemetryValueSettings { protected getTelemetryValueSettings(): TelemetryValueSettings {
return this.settings.getTimeSeries; return this.settings.getTimeSeries;
} }
protected doGetTelemetryValue(): Observable<V> {
const getTelemetryValueSettings = this.getTelemetryValueSettings();
return this.ctx.attributeService.getEntityTimeseriesLatest(this.targetEntityId,
[getTelemetryValueSettings.key], true, {ignoreLoading: true, ignoreErrors: true})
.pipe(
map((data) => {
let value: any = null;
if (data[getTelemetryValueSettings.key]) {
const dataSet = data[getTelemetryValueSettings.key];
if (dataSet.length) {
value = dataSet[0].value;
}
}
return value;
})
);
}
} }
export class ExecuteRpcValueSetter<V> extends ValueSetter<V> { export class ExecuteRpcValueSetter<V> extends ValueSetter<V> {

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

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

@ -0,0 +1,63 @@
///
/// 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
},
getTimeSeries: {
key: 'state'
},
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
},
getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
}
};

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

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<div #singleSwitchPanel class="tb-single-switch-panel" [style.pointer-events]="ctx.isEdit ? 'none' : 'all'" [style]="backgroundStyle$ | async"> <div #singleSwitchPanel class="tb-single-switch-panel" [class.auto-scale]="autoScale" [style.pointer-events]="ctx.isEdit ? 'none' : 'all'" [style]="backgroundStyle$ | async">
<div class="tb-single-switch-overlay" [style]="overlayStyle"></div> <div class="tb-single-switch-overlay" [style]="overlayStyle" [style.inset]="overlayInset"></div>
<div class="tb-single-switch-title-panel"> <div class="tb-single-switch-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container> <ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div> </div>
@ -27,16 +27,16 @@
</div> </div>
<div #singleSwitchToggleRow class="tb-single-switch-slide-toggle-row"> <div #singleSwitchToggleRow class="tb-single-switch-slide-toggle-row">
<div *ngIf="showOffLabel" [style]="offLabelStyle">{{ offLabel }}</div> <div *ngIf="showOffLabel" [style]="offLabelStyle">{{ offLabel }}</div>
<mat-slide-toggle class="tb-single-switch-toggle" [disabled]="loading$ | async" [(ngModel)]="value" (click)="onToggleChange($event)"> <mat-slide-toggle class="tb-single-switch-toggle" [disabled]="disabled || (loading$ | async)" [(ngModel)]="value" (click)="!disabled && onToggleChange($event)">
</mat-slide-toggle> </mat-slide-toggle>
<div *ngIf="showOnLabel" [style]="onLabelStyle">{{ onLabel }}</div> <div *ngIf="showOnLabel" [style]="onLabelStyle">{{ onLabel }}</div>
</div> </div>
</div> </div>
<mat-progress-bar class="tb-single-switch-progress" style="height: 2px;" color="accent" mode="indeterminate" *ngIf="loading$ | async"></mat-progress-bar> <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 *ngIf="error" class="tb-action-widget-error-container">
<div class="tb-single-switch-error-panel"> <div class="tb-action-widget-error-panel">
<div class="tb-single-switch-error-text" [innerHTML]="error | safe: 'html'"></div> <div class="tb-action-widget-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> <button class="tb-action-widget-error-clear tb-mat-20" mat-icon-button (click)="clearError()"><mat-icon>close</mat-icon></button>
</div> </div>
</div> </div>
</div> </div>

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

@ -29,15 +29,15 @@ $switchColorDisabled: var(--tb-single-switch-color-disabled, #D5D7E5);
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 18px 24px; padding: 18px 24px;
&.auto-scale {
padding: 0;
}
> div:not(.tb-single-switch-overlay), > tb-icon { > div:not(.tb-single-switch-overlay), > tb-icon {
z-index: 1; z-index: 1;
} }
.tb-single-switch-overlay { .tb-single-switch-overlay {
position: absolute; position: absolute;
top: 12px; inset: 12px;
left: 12px;
bottom: 12px;
right: 12px;
} }
.tb-single-switch-progress { .tb-single-switch-progress {
position: absolute; position: absolute;
@ -45,36 +45,6 @@ $switchColorDisabled: var(--tb-single-switch-color-disabled, #D5D7E5);
left: 0; left: 0;
right: 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 { > div.tb-single-switch-title-panel {
position: absolute; position: absolute;
top: 12px; top: 12px;

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

@ -42,17 +42,17 @@ import { Observable } from 'rxjs';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import cssjs from '@core/css/css';
import { hashCode } from '@core/utils';
import { ValueType } from '@shared/models/constants'; import { ValueType } from '@shared/models/constants';
import { UtilsService } from '@core/services/utils.service';
const initialSwitchHeight = 60;
const horizontalLayoutPadding = 48; const horizontalLayoutPadding = 48;
const verticalLayoutPadding = 36; const verticalLayoutPadding = 36;
@Component({ @Component({
selector: 'tb-single-switch-widget', selector: 'tb-single-switch-widget',
templateUrl: './single-switch-widget.component.html', 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 encapsulation: ViewEncapsulation.None
}) })
export class SingleSwitchWidgetComponent extends export class SingleSwitchWidgetComponent extends
@ -74,8 +74,10 @@ export class SingleSwitchWidgetComponent extends
backgroundStyle$: Observable<ComponentStyle>; backgroundStyle$: Observable<ComponentStyle>;
overlayStyle: ComponentStyle = {}; overlayStyle: ComponentStyle = {};
overlayInset = '12px';
value = false; value = false;
disabled = false;
layout: SingleSwitchLayout; layout: SingleSwitchLayout;
@ -102,9 +104,12 @@ export class SingleSwitchWidgetComponent extends
private onValueSetter: ValueSetter<boolean>; private onValueSetter: ValueSetter<boolean>;
private offValueSetter: ValueSetter<boolean>; private offValueSetter: ValueSetter<boolean>;
private singleSwitchCssClass: string;
constructor(protected imagePipe: ImagePipe, constructor(protected imagePipe: ImagePipe,
protected sanitizer: DomSanitizer, protected sanitizer: DomSanitizer,
private renderer: Renderer2, private renderer: Renderer2,
private utils: UtilsService,
protected cd: ChangeDetectorRef, protected cd: ChangeDetectorRef,
private elementRef: ElementRef) { private elementRef: ElementRef) {
super(cd); super(cd);
@ -148,25 +153,27 @@ export class SingleSwitchWidgetComponent extends
`--tb-single-switch-color-off: ${this.settings.switchColorOff};\n`+ `--tb-single-switch-color-off: ${this.settings.switchColorOff};\n`+
`--tb-single-switch-color-disabled: ${this.settings.switchColorDisabled};\n`+ `--tb-single-switch-color-disabled: ${this.settings.switchColorDisabled};\n`+
`}`; `}`;
const cssParser = new cssjs(); this.singleSwitchCssClass =
cssParser.testMode = false; this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement, 'tb-single-switch', switchVariablesCss);
const namespace = 'single-switch-' + hashCode(switchVariablesCss);
cssParser.cssPreviewNamespace = namespace;
cssParser.createStyleElement(namespace, switchVariablesCss);
this.renderer.addClass(this.elementRef.nativeElement, namespace);
const getInitialStateSettings = 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, { this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onValue(value) next: (value) => this.onValue(value)
}); });
const disabledStateSettings =
{...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')};
this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, {
next: (value) => this.onDisabled(value)
});
const onUpdateStateSettings = {...this.settings.onUpdateState, 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); this.onValueSetter = this.createValueSetter(onUpdateStateSettings);
const offUpdateStateSettings = {...this.settings.offUpdateState, 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); this.offValueSetter = this.createValueSetter(offUpdateStateSettings);
} }
@ -190,6 +197,9 @@ export class SingleSwitchWidgetComponent extends
if (this.panelResize$) { if (this.panelResize$) {
this.panelResize$.disconnect(); this.panelResize$.disconnect();
} }
if (this.singleSwitchCssClass) {
this.utils.clearCssElement(this.renderer, this.singleSwitchCssClass);
}
super.ngOnDestroy(); super.ngOnDestroy();
} }
@ -211,25 +221,63 @@ export class SingleSwitchWidgetComponent extends
} }
private onValue(value: boolean): void { private onValue(value: boolean): void {
console.log(`onValue: ${value}`);
this.value = !!value; this.value = !!value;
this.cd.markForCheck(); this.cd.markForCheck();
} }
private onDisabled(value: boolean): void {
this.disabled = !!value;
if (this.disabled) {
if (this.showLabel) {
this.labelStyle = {...this.labelStyle, color: 'rgba(0, 0, 0, 0.38)'};
}
if (this.showIcon) {
this.iconStyle = {...this.iconStyle, color: 'rgba(0, 0, 0, 0.38)'};
}
if (this.showOnLabel) {
this.onLabelStyle = {...this.onLabelStyle, color: 'rgba(0, 0, 0, 0.38)'};
}
if (this.showOffLabel) {
this.offLabelStyle = {...this.offLabelStyle, color: 'rgba(0, 0, 0, 0.38)'};
}
} else {
if (this.showLabel) {
this.labelStyle = {...this.labelStyle, color: this.settings.labelColor};
}
if (this.showIcon) {
this.iconStyle = {...this.iconStyle, color: this.settings.iconColor};
}
if (this.showOnLabel) {
this.onLabelStyle = {...this.onLabelStyle, color: this.settings.onLabelColor};
}
if (this.showOffLabel) {
this.offLabelStyle = {...this.offLabelStyle, color: this.settings.offLabelColor};
}
}
this.cd.markForCheck();
}
private onResize() { private onResize() {
const panelWidth = this.singleSwitchPanel.nativeElement.getBoundingClientRect().width - horizontalLayoutPadding; const height = this.singleSwitchPanel.nativeElement.getBoundingClientRect().height;
const panelHeight = this.singleSwitchPanel.nativeElement.getBoundingClientRect().height - verticalLayoutPadding; const switchScale = height / initialSwitchHeight;
const paddingScale = Math.min(switchScale, 1);
const panelWidth = this.singleSwitchPanel.nativeElement.getBoundingClientRect().width - (horizontalLayoutPadding * paddingScale);
const panelHeight = this.singleSwitchPanel.nativeElement.getBoundingClientRect().height - (verticalLayoutPadding * paddingScale);
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'transform', `scale(1)`); this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'transform', `scale(1)`);
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'width', 'auto');
let contentWidth = this.singleSwitchToggleRow.nativeElement.getBoundingClientRect().width; let contentWidth = this.singleSwitchToggleRow.nativeElement.getBoundingClientRect().width;
let contentHeight = this.singleSwitchToggleRow.nativeElement.getBoundingClientRect().height; let contentHeight = this.singleSwitchToggleRow.nativeElement.getBoundingClientRect().height;
if (this.showIcon || this.showLabel) { if (this.showIcon || this.showLabel) {
contentWidth += (8 + this.singleSwitchLabelRow.nativeElement.getBoundingClientRect().width); contentWidth += (8 + this.singleSwitchLabelRow.nativeElement.getBoundingClientRect().width);
contentHeight = Math.max(contentHeight, this.singleSwitchLabelRow.nativeElement.getBoundingClientRect().height); contentHeight = Math.max(contentHeight, this.singleSwitchLabelRow.nativeElement.getBoundingClientRect().height);
} }
const scale = Math.min(panelWidth / contentWidth, panelHeight / contentHeight); const maxScale = Math.max(1, switchScale);
const scale = Math.min(Math.min(panelWidth / contentWidth, panelHeight / contentHeight), maxScale);
const width = panelWidth / scale; const width = panelWidth / scale;
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'width', width + 'px'); this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'width', width + 'px');
this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'transform', `scale(${scale})`); this.renderer.setStyle(this.singleSwitchContent.nativeElement, 'transform', `scale(${scale})`);
this.overlayInset = (Math.floor(12 * paddingScale * 100) / 100) + 'px';
this.cd.markForCheck();
} }
} }

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

@ -51,6 +51,7 @@ export const singleSwitchLayoutImages = new Map<SingleSwitchLayout, string>(
export interface SingleSwitchWidgetSettings { export interface SingleSwitchWidgetSettings {
initialState: GetValueSettings<boolean>; initialState: GetValueSettings<boolean>;
disabledState: GetValueSettings<boolean>;
onUpdateState: SetValueSettings; onUpdateState: SetValueSettings;
offUpdateState: SetValueSettings; offUpdateState: SetValueSettings;
layout: SingleSwitchLayout; layout: SingleSwitchLayout;
@ -93,12 +94,26 @@ export const singleSwitchDefaultSettings: SingleSwitchWidgetSettings = {
}, },
getAttribute: { getAttribute: {
key: 'state', key: 'state',
scope: null, scope: null
subscribeForUpdates: false
}, },
getTimeSeries: { getTimeSeries: {
key: 'state'
},
dataToValue: {
type: DataToValueType.NONE,
compareToValue: true,
dataToValueFunction: '/* Should return boolean value */\nreturn data;'
}
},
disabledState: {
action: GetValueAction.DO_NOTHING,
defaultValue: false,
getAttribute: {
key: 'state', key: 'state',
subscribeForUpdates: false scope: null
},
getTimeSeries: {
key: 'state'
}, },
dataToValue: { dataToValue: {
type: DataToValueType.NONE, type: DataToValueType.NONE,

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

@ -0,0 +1,51 @@
<!--
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]="actionButtonWidgetSettingsForm">
<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.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]="borderRadius"
formControlName="appearance">
</tb-widget-button-appearance>
</div>
</ng-container>

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

@ -0,0 +1,70 @@
///
/// 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 { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ValueType } from '@shared/models/constants';
import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models';
import { actionButtonDefaultSettings } from '@home/components/widget/lib/button/action-button-widget.models';
@Component({
selector: 'tb-action-button-widget-settings',
templateUrl: './action-button-widget-settings.component.html',
styleUrls: ['./../widget-settings.scss']
})
export class ActionButtonWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice {
const datasources = this.widgetConfig?.config?.datasources;
return getTargetDeviceFromDatasources(datasources);
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
}
get borderRadius(): string {
return this.widgetConfig?.config?.borderRadius;
}
valueType = ValueType;
actionButtonWidgetSettingsForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.actionButtonWidgetSettingsForm;
}
protected defaultSettings(): WidgetSettings {
return {...actionButtonDefaultSettings};
}
protected onSettingsSet(settings: WidgetSettings) {
this.actionButtonWidgetSettingsForm = this.fb.group({
activatedState: [settings.activatedState, []],
disabledState: [settings.disabledState, []],
appearance: [settings.appearance, []]
});
}
}

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" <button #matButton [disabled]="disabled"
class="tb-value-action-settings" class="tb-action-settings-button"
mat-stroked-button color="primary" mat-stroked-button color="primary"
(click)="openValueActionSettingsPopup($event, matButton)"> (click)="openActionSettingsPopup($event, matButton)">
<div> <div>
<span>{{ displayValue }}</span> <span>{{ displayValue }}</span>
<mat-icon class="tb-mat-20">edit</mat-icon> <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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
.mdc-button.tb-value-action-settings { .mdc-button.tb-action-settings-button {
width: 100%; width: 100%;
.mdc-button__label { .mdc-button__label {
width: 100%; 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'; @import '../../../../../../../../../scss/constants';
.tb-value-action-settings-panel { .tb-action-settings-panel {
width: 530px; width: 530px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -23,20 +23,20 @@
@media #{$mat-lt-md} { @media #{$mat-lt-md} {
width: 90vw; width: 90vw;
} }
.tb-value-action-settings-panel-content { .tb-action-settings-panel-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
overflow: auto; overflow: auto;
} }
.tb-value-action-settings-title { .tb-action-settings-title {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
line-height: 24px; line-height: 24px;
letter-spacing: 0.25px; letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87); color: rgba(0, 0, 0, 0.87);
} }
.tb-value-action-settings-panel-buttons { .tb-action-settings-panel-buttons {
height: 40px; height: 40px;
display: flex; display: flex;
flex-direction: row; 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 { .gutter.gutter-horizontal {
cursor: col-resize; 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, .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 // 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 { import {
AfterViewInit, AfterViewInit,
@ -35,7 +35,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { CustomActionDescriptor } from '@shared/models/widget.models'; 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({ @Component({
selector: 'tb-custom-action-pretty-editor', 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 { Ace } from 'ace-builds';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { ResizeObserver } from '@juggle/resize-observer'; 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 { Observable } from 'rxjs/internal/Observable';
import { forkJoin, from } from 'rxjs'; import { forkJoin, from } from 'rxjs';
import { map, tap } from 'rxjs/operators'; 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

24
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. limitations under the License.
--> -->
<div class="tb-value-action-settings-panel" [formGroup]="getValueSettingsFormGroup"> <div class="tb-action-settings-panel" [formGroup]="getValueSettingsFormGroup">
<div class="tb-value-action-settings-title">{{ panelTitle | translate }}</div> <div class="tb-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-value-action-settings-panel-content"> <div class="tb-action-settings-panel-content">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" >{{ 'widgets.value-action.action' | translate }}</div> <div class="fixed-title-width" >{{ 'widgets.value-action.action' | translate }}</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
@ -33,8 +33,8 @@
<div class="tb-form-row space-between"> <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> <div tb-hint-tooltip-icon="{{'widgets.value-action.init-value-hint' | translate}}" translate>widgets.value-action.value</div>
<tb-value-input [valueType]="valueType" <tb-value-input [valueType]="valueType"
trueLabel="widgets.value-action.on" [trueLabel]="trueLabel"
falseLabel="widgets.value-action.off" [falseLabel]="falseLabel"
formControlName="defaultValue"></tb-value-input> formControlName="defaultValue"></tb-value-input>
</div> </div>
</ng-template> </ng-template>
@ -84,11 +84,6 @@
[attributeScope]="getValueSettingsFormGroup.get('getAttribute').get('scope').value"> [attributeScope]="getValueSettingsFormGroup.get('getAttribute').get('scope').value">
</tb-device-key-autocomplete> </tb-device-key-autocomplete>
</div> </div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="subscribeForUpdates">
<span tb-hint-tooltip-icon="{{'widgets.value-action.subscribe-for-updates-hint' | translate}}">{{ 'widgets.value-action.subscribe-for-updates' | translate }}</span>
</mat-slide-toggle>
</div>
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="getValueAction.GET_TIME_SERIES"> <ng-template [ngSwitchCase]="getValueAction.GET_TIME_SERIES">
@ -106,11 +101,6 @@
[keyType]="dataKeyType.timeseries"> [keyType]="dataKeyType.timeseries">
</tb-device-key-autocomplete> </tb-device-key-autocomplete>
</div> </div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="subscribeForUpdates">
<span tb-hint-tooltip-icon="{{'widgets.value-action.subscribe-for-updates-hint' | translate}}">{{ 'widgets.value-action.subscribe-for-updates' | translate }}</span>
</mat-slide-toggle>
</div>
</ng-container> </ng-container>
</ng-template> </ng-template>
</ng-container> </ng-container>
@ -132,7 +122,7 @@
helpId="widget/lib/rpc/parse_value_fn"> helpId="widget/lib/rpc/parse_value_fn">
</tb-js-func> </tb-js-func>
<div *ngIf="valueType === ValueType.BOOLEAN" class="tb-form-row align-start no-gap column-xs"> <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 <tb-value-input
fxFlex fxFlex
layout="column" layout="column"
@ -203,7 +193,7 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
</div> </div>
<div class="tb-value-action-settings-panel-buttons"> <div class="tb-action-settings-panel-buttons">
<button mat-button <button mat-button
color="primary" color="primary"
type="button" type="button"

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

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 GetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component'; } from '@home/components/widget/lib/settings/common/action/get-value-action-settings-panel.component';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { TargetDevice } from '@shared/models/widget.models'; import { TargetDevice, widgetType } from '@shared/models/widget.models';
@Component({ @Component({
selector: 'tb-get-value-action-settings', selector: 'tb-get-value-action-settings',
templateUrl: './value-action-settings-button.component.html', templateUrl: './action-settings-button.component.html',
styleUrls: ['./value-action-settings-button.scss'], styleUrls: ['./action-settings-button.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -61,12 +61,24 @@ export class GetValueActionSettingsComponent implements OnInit, ControlValueAcce
@Input() @Input()
valueType: ValueType; valueType: ValueType;
@Input()
trueLabel = 'value.true';
@Input()
falseLabel = 'value.false';
@Input()
stateLabel: string;
@Input() @Input()
aliasController: IAliasController; aliasController: IAliasController;
@Input() @Input()
targetDevice: TargetDevice; targetDevice: TargetDevice;
@Input()
widgetType: widgetType;
@Input() @Input()
disabled = false; disabled = false;
@ -103,7 +115,7 @@ export class GetValueActionSettingsComponent implements OnInit, ControlValueAcce
this.updateDisplayValue(); this.updateDisplayValue();
} }
openValueActionSettingsPopup($event: Event, matButton: MatButton) { openActionSettingsPopup($event: Event, matButton: MatButton) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -115,8 +127,12 @@ export class GetValueActionSettingsComponent implements OnInit, ControlValueAcce
getValueSettings: this.modelValue, getValueSettings: this.modelValue,
panelTitle: this.panelTitle, panelTitle: this.panelTitle,
valueType: this.valueType, valueType: this.valueType,
trueLabel: this.trueLabel,
falseLabel: this.falseLabel,
stateLabel: this.stateLabel,
aliasController: this.aliasController, aliasController: this.aliasController,
targetDevice: this.targetDevice targetDevice: this.targetDevice,
widgetType: this.widgetType
}; };
const getValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, const getValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, GetValueActionSettingsPanelComponent, this.viewContainerRef, GetValueActionSettingsPanelComponent,
@ -138,7 +154,8 @@ export class GetValueActionSettingsComponent implements OnInit, ControlValueAcce
switch (this.modelValue.action) { switch (this.modelValue.action) {
case GetValueAction.DO_NOTHING: case GetValueAction.DO_NOTHING:
if (this.valueType === ValueType.BOOLEAN) { 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 { } else {
this.displayValue = this.modelValue.defaultValue + ''; 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, WidgetMobileActionType,
widgetMobileActionTypeTranslationMap widgetMobileActionTypeTranslationMap
} from '@shared/models/widget.models'; } 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 { import {
getDefaultGetLocationFunction, getDefaultGetLocationFunction,
getDefaultGetPhoneNumberFunction, getDefaultGetPhoneNumberFunction,
@ -39,7 +39,7 @@ import {
getDefaultProcessLaunchResultFunction, getDefaultProcessLaunchResultFunction,
getDefaultProcessLocationFunction, getDefaultProcessLocationFunction,
getDefaultProcessQrCodeFunction 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'; import { WidgetService } from '@core/http/widget.service';
@Component({ @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. limitations under the License.
--> -->
<div class="tb-value-action-settings-panel" [formGroup]="setValueSettingsFormGroup"> <div class="tb-action-settings-panel" [formGroup]="setValueSettingsFormGroup">
<div class="tb-value-action-settings-title">{{ panelTitle | translate }}</div> <div class="tb-action-settings-title">{{ panelTitle | translate }}</div>
<div class="tb-value-action-settings-panel-content"> <div class="tb-action-settings-panel-content">
<div class="tb-form-row"> <div class="tb-form-row">
<div class="fixed-title-width" >{{ 'widgets.value-action.action' | translate }}</div> <div class="fixed-title-width" >{{ 'widgets.value-action.action' | translate }}</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
@ -183,7 +183,7 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
</div> </div>
<div class="tb-value-action-settings-panel-buttons"> <div class="tb-action-settings-panel-buttons">
<button mat-button <button mat-button
color="primary" color="primary"
type="button" 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 { import {
getValueActions, getValueActions,
SetValueAction, SetValueAction,
setValueActions, setValueActions, setValueActionsByWidgetType,
setValueActionTranslations, setValueActionTranslations,
SetValueSettings, SetValueSettings,
ValueToDataType ValueToDataType
} from '@shared/models/action-widget-settings.models'; } from '@shared/models/action-widget-settings.models';
import { ValueType } from '@shared/models/constants'; 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 { AttributeScope, DataKeyType, telemetryTypeTranslationsShort } from '@shared/models/telemetry/telemetry.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { WidgetService } from '@core/http/widget.service'; 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', selector: 'tb-set-value-action-settings-panel',
templateUrl: './set-value-action-settings-panel.component.html', templateUrl: './set-value-action-settings-panel.component.html',
providers: [], providers: [],
styleUrls: ['./value-action-settings-panel.component.scss'], styleUrls: ['./action-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class SetValueActionSettingsPanelComponent extends PageComponent implements OnInit { export class SetValueActionSettingsPanelComponent extends PageComponent implements OnInit {
@ -59,6 +59,9 @@ export class SetValueActionSettingsPanelComponent extends PageComponent implemen
@Input() @Input()
targetDevice: TargetDevice; targetDevice: TargetDevice;
@Input()
widgetType: widgetType;
@Input() @Input()
popover: TbPopoverComponent<SetValueActionSettingsPanelComponent>; popover: TbPopoverComponent<SetValueActionSettingsPanelComponent>;
@ -67,7 +70,7 @@ export class SetValueActionSettingsPanelComponent extends PageComponent implemen
setValueAction = SetValueAction; setValueAction = SetValueAction;
setValueActions = setValueActions; setValueActions: SetValueAction[];
setValueActionTranslationsMap = setValueActionTranslations; setValueActionTranslationsMap = setValueActionTranslations;
@ -92,6 +95,7 @@ export class SetValueActionSettingsPanelComponent extends PageComponent implemen
} }
ngOnInit(): void { ngOnInit(): void {
this.setValueActions = setValueActionsByWidgetType(this.widgetType);
this.setValueSettingsFormGroup = this.fb.group( this.setValueSettingsFormGroup = this.fb.group(
{ {
action: [this.setValueSettings?.action, []], 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 { TranslateService } from '@ngx-translate/core';
import { ValueType } from '@shared/models/constants'; import { ValueType } from '@shared/models/constants';
import { IAliasController } from '@core/api/widget-api.models'; 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 { isDefinedAndNotNull } from '@core/utils';
import { import {
SetValueActionSettingsPanelComponent SetValueActionSettingsPanelComponent
@ -40,8 +40,8 @@ import {
@Component({ @Component({
selector: 'tb-set-value-action-settings', selector: 'tb-set-value-action-settings',
templateUrl: './value-action-settings-button.component.html', templateUrl: './action-settings-button.component.html',
styleUrls: ['./value-action-settings-button.scss'], styleUrls: ['./action-settings-button.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -68,6 +68,9 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce
@Input() @Input()
targetDevice: TargetDevice; targetDevice: TargetDevice;
@Input()
widgetType: widgetType;
@Input() @Input()
disabled = false; disabled = false;
@ -104,7 +107,7 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce
this.updateDisplayValue(); this.updateDisplayValue();
} }
openValueActionSettingsPopup($event: Event, matButton: MatButton) { openActionSettingsPopup($event: Event, matButton: MatButton) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
@ -117,7 +120,8 @@ export class SetValueActionSettingsComponent implements OnInit, ControlValueAcce
panelTitle: this.panelTitle, panelTitle: this.panelTitle,
valueType: this.valueType, valueType: this.valueType,
aliasController: this.aliasController, aliasController: this.aliasController,
targetDevice: this.targetDevice targetDevice: this.targetDevice,
widgetType: this.widgetType
}; };
const setValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer, const setValueSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, SetValueActionSettingsPanelComponent, 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 { import {
CustomActionEditorCompleter, CustomActionEditorCompleter,
toCustomAction 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; const stateDisplayTypes = ['normal', 'separateDialog', 'popover'] as const;
type stateDisplayTypeTuple = typeof stateDisplayTypes; 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 = deepClone(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;
}
}
}
}

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

@ -0,0 +1,158 @@
///
/// 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';
import { deepClone } from '@core/utils';
@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 = deepClone(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 SetValueActionSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/action/set-value-action-settings-panel.component'; } 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 { 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({ @NgModule({
declarations: [ declarations: [
@ -93,7 +118,16 @@ import { CssSizeInputComponent } from '@home/components/widget/lib/settings/comm
GetValueActionSettingsPanelComponent, GetValueActionSettingsPanelComponent,
DeviceKeyAutocompleteComponent, DeviceKeyAutocompleteComponent,
SetValueActionSettingsComponent, SetValueActionSettingsComponent,
SetValueActionSettingsPanelComponent SetValueActionSettingsPanelComponent,
WidgetActionComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent,
WidgetActionSettingsComponent,
WidgetActionSettingsPanelComponent,
WidgetButtonAppearanceComponent,
WidgetButtonCustomStyleComponent,
WidgetButtonCustomStylePanelComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -124,7 +158,16 @@ import { CssSizeInputComponent } from '@home/components/widget/lib/settings/comm
GetValueActionSettingsPanelComponent, GetValueActionSettingsPanelComponent,
DeviceKeyAutocompleteComponent, DeviceKeyAutocompleteComponent,
SetValueActionSettingsComponent, SetValueActionSettingsComponent,
SetValueActionSettingsPanelComponent SetValueActionSettingsPanelComponent,
WidgetActionComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
MobileActionEditorComponent,
WidgetActionSettingsComponent,
WidgetActionSettingsPanelComponent,
WidgetButtonAppearanceComponent,
WidgetButtonCustomStyleComponent,
WidgetButtonCustomStylePanelComponent
], ],
providers: [ providers: [
ColorSettingsComponentService, ColorSettingsComponentService,

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/led-indicator-widget-settings.component.ts

@ -32,7 +32,7 @@ export class LedIndicatorWidgetSettingsComponent extends WidgetSettingsComponent
functionScopeVariables = this.widgetService.getWidgetScopeVariables(); functionScopeVariables = this.widgetService.getWidgetScopeVariables();
get targetDevice(): TargetDevice { get targetDevice(): TargetDevice {
return this.widget?.config?.targetDevice; return this.widgetConfig?.config?.targetDevice;
} }
dataKeyType = DataKeyType; dataKeyType = DataKeyType;

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

@ -37,7 +37,7 @@ export class RoundSwitchWidgetSettingsComponent extends WidgetSettingsComponent
} }
get targetDevice(): TargetDevice { get targetDevice(): TargetDevice {
return this.widget?.config?.targetDevice; return this.widgetConfig?.config?.targetDevice;
} }
protected settingsForm(): UntypedFormGroup { protected settingsForm(): UntypedFormGroup {

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

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

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

@ -15,12 +15,13 @@
/// ///
import { Component } from '@angular/core'; 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 { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
singleSwitchDefaultSettings, singleSwitchLayoutImages, singleSwitchDefaultSettings,
singleSwitchLayoutImages,
singleSwitchLayouts, singleSwitchLayouts,
singleSwitchLayoutTranslations singleSwitchLayoutTranslations
} from '@home/components/widget/lib/rpc/single-switch-widget.models'; } from '@home/components/widget/lib/rpc/single-switch-widget.models';
@ -34,7 +35,11 @@ import { ValueType } from '@shared/models/constants';
export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent { export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent {
get targetDevice(): TargetDevice { get targetDevice(): TargetDevice {
return this.widget?.config?.targetDevice; return this.widgetConfig?.config?.targetDevice;
}
get widgetType(): widgetType {
return this.widgetConfig?.widgetType;
} }
singleSwitchLayouts = singleSwitchLayouts; singleSwitchLayouts = singleSwitchLayouts;
@ -64,6 +69,7 @@ export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent
initialState: [settings.initialState, []], initialState: [settings.initialState, []],
onUpdateState: [settings.onUpdateState, []], onUpdateState: [settings.onUpdateState, []],
offUpdateState: [settings.offUpdateState, []], offUpdateState: [settings.offUpdateState, []],
disabledState: [settings.disabledState, []],
layout: [settings.layout, []], layout: [settings.layout, []],
autoScale: [settings.autoScale, []], autoScale: [settings.autoScale, []],
@ -104,7 +110,7 @@ export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent
return ['showLabel', 'showIcon', 'showOnLabel', 'showOffLabel']; return ['showLabel', 'showIcon', 'showOnLabel', 'showOffLabel'];
} }
protected updateValidators(emitEvent: boolean): void { protected updateValidators(_emitEvent: boolean): void {
const showLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showLabel').value; const showLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showLabel').value;
const showIcon: boolean = this.singleSwitchWidgetSettingsForm.get('showIcon').value; const showIcon: boolean = this.singleSwitchWidgetSettingsForm.get('showIcon').value;
const showOnLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showOnLabel').value; const showOnLabel: boolean = this.singleSwitchWidgetSettingsForm.get('showOnLabel').value;
@ -152,6 +158,4 @@ export class SingleSwitchWidgetSettingsComponent extends WidgetSettingsComponent
this.singleSwitchWidgetSettingsForm.get('offLabelColor').disable(); this.singleSwitchWidgetSettingsForm.get('offLabelColor').disable();
} }
} }
protected readonly ValueType = ValueType;
} }

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/slide-toggle-widget-settings.component.ts

@ -37,7 +37,7 @@ export class SlideToggleWidgetSettingsComponent extends WidgetSettingsComponent
} }
get targetDevice(): TargetDevice { get targetDevice(): TargetDevice {
return this.widget?.config?.targetDevice; return this.widgetConfig?.config?.targetDevice;
} }
protected settingsForm(): UntypedFormGroup { protected settingsForm(): UntypedFormGroup {

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

@ -37,7 +37,7 @@ export class SwitchControlWidgetSettingsComponent extends WidgetSettingsComponen
} }
get targetDevice(): TargetDevice { get targetDevice(): TargetDevice {
return this.widget?.config?.targetDevice; return this.widgetConfig?.config?.targetDevice;
} }
protected settingsForm(): UntypedFormGroup { protected settingsForm(): UntypedFormGroup {

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

@ -15,7 +15,9 @@
/// ///
import { NgModule, Type } from '@angular/core'; import { NgModule, Type } from '@angular/core';
import { QrCodeWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/qrcode-widget-settings.component'; import {
QrCodeWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/qrcode-widget-settings.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module';
@ -33,7 +35,9 @@ import {
MarkdownWidgetSettingsComponent MarkdownWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/markdown-widget-settings.component'; } from '@home/components/widget/lib/settings/cards/markdown-widget-settings.component';
import { LabelWidgetLabelComponent } from '@home/components/widget/lib/settings/cards/label-widget-label.component'; import { LabelWidgetLabelComponent } from '@home/components/widget/lib/settings/cards/label-widget-label.component';
import { LabelWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/label-widget-settings.component'; import {
LabelWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/label-widget-settings.component';
import { import {
SimpleCardWidgetSettingsComponent SimpleCardWidgetSettingsComponent
} from '@home/components/widget/lib/settings/cards/simple-card-widget-settings.component'; } from '@home/components/widget/lib/settings/cards/simple-card-widget-settings.component';
@ -311,6 +315,9 @@ import {
import { import {
SingleSwitchWidgetSettingsComponent SingleSwitchWidgetSettingsComponent
} from '@home/components/widget/lib/settings/control/single-switch-widget-settings.component'; } from '@home/components/widget/lib/settings/control/single-switch-widget-settings.component';
import {
ActionButtonWidgetSettingsComponent
} from '@home/components/widget/lib/settings/button/action-button-widget-settings.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -424,7 +431,8 @@ import {
DoughnutWidgetSettingsComponent, DoughnutWidgetSettingsComponent,
RangeChartWidgetSettingsComponent, RangeChartWidgetSettingsComponent,
BarChartWithLabelsWidgetSettingsComponent, BarChartWithLabelsWidgetSettingsComponent,
SingleSwitchWidgetSettingsComponent SingleSwitchWidgetSettingsComponent,
ActionButtonWidgetSettingsComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -543,7 +551,8 @@ import {
DoughnutWidgetSettingsComponent, DoughnutWidgetSettingsComponent,
RangeChartWidgetSettingsComponent, RangeChartWidgetSettingsComponent,
BarChartWithLabelsWidgetSettingsComponent, BarChartWithLabelsWidgetSettingsComponent,
SingleSwitchWidgetSettingsComponent SingleSwitchWidgetSettingsComponent,
ActionButtonWidgetSettingsComponent
] ]
}) })
export class WidgetSettingsModule { export class WidgetSettingsModule {
@ -629,5 +638,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-doughnut-widget-settings': DoughnutWidgetSettingsComponent, 'tb-doughnut-widget-settings': DoughnutWidgetSettingsComponent,
'tb-range-chart-widget-settings': RangeChartWidgetSettingsComponent, 'tb-range-chart-widget-settings': RangeChartWidgetSettingsComponent,
'tb-bar-chart-with-labels-widget-settings': BarChartWithLabelsWidgetSettingsComponent, 'tb-bar-chart-with-labels-widget-settings': BarChartWithLabelsWidgetSettingsComponent,
'tb-single-switch-widget-settings': SingleSwitchWidgetSettingsComponent 'tb-single-switch-widget-settings': SingleSwitchWidgetSettingsComponent,
'tb-action-button-widget-settings': ActionButtonWidgetSettingsComponent
}; };

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)) { if (isUndefined(result.typeParameters.embedTitlePanel)) {
result.typeParameters.embedTitlePanel = false; result.typeParameters.embedTitlePanel = false;
} }
if (isUndefined(result.typeParameters.overflowVisible)) {
result.typeParameters.overflowVisible = false;
}
if (isUndefined(result.typeParameters.hideDataSettings)) { if (isUndefined(result.typeParameters.hideDataSettings)) {
result.typeParameters.hideDataSettings = false; result.typeParameters.hideDataSettings = false;
} }

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

@ -71,6 +71,7 @@ import {
BarChartWithLabelsWidgetComponent BarChartWithLabelsWidgetComponent
} from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.component'; } 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 { SingleSwitchWidgetComponent } from '@home/components/widget/lib/rpc/single-switch-widget.component';
import { ActionButtonWidgetComponent } from '@home/components/widget/lib/button/action-button-widget.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -114,7 +115,8 @@ import { SingleSwitchWidgetComponent } from '@home/components/widget/lib/rpc/sin
DoughnutWidgetComponent, DoughnutWidgetComponent,
RangeChartWidgetComponent, RangeChartWidgetComponent,
BarChartWithLabelsWidgetComponent, BarChartWithLabelsWidgetComponent,
SingleSwitchWidgetComponent SingleSwitchWidgetComponent,
ActionButtonWidgetComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -162,7 +164,8 @@ import { SingleSwitchWidgetComponent } from '@home/components/widget/lib/rpc/sin
DoughnutWidgetComponent, DoughnutWidgetComponent,
RangeChartWidgetComponent, RangeChartWidgetComponent,
BarChartWithLabelsWidgetComponent, BarChartWithLabelsWidgetComponent,
SingleSwitchWidgetComponent SingleSwitchWidgetComponent,
ActionButtonWidgetComponent
], ],
providers: [ providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } {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-highlighted': isHighlighted(widget),
'tb-not-highlighted': isNotHighlighted(widget), 'tb-not-highlighted': isNotHighlighted(widget),
'mat-elevation-z4': widget.dropShadow, 'mat-elevation-z4': widget.dropShadow,
'tb-overflow-visible': widgetComponent.widgetContext?.overflowVisible,
'tb-has-timewindow': widget.hasTimewindow, 'tb-has-timewindow': widget.hasTimewindow,
'tb-edit': isEdit 'tb-edit': isEdit
}" }"
@ -88,6 +89,7 @@
<tb-widget #widgetComponent <tb-widget #widgetComponent
[dashboardWidget]="widget" [dashboardWidget]="widget"
[isEdit]="isEdit" [isEdit]="isEdit"
[isPreview]="isPreview"
[isMobile]="isMobile" [isMobile]="isMobile"
[widgetTitlePanel]="widgetTitlePanel"> [widgetTitlePanel]="widgetTitlePanel">
</tb-widget> </tb-widget>

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

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

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

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

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

@ -25,6 +25,7 @@
[autofillHeight]="true" [autofillHeight]="true"
[columns]="24" [columns]="24"
[isEdit]="false" [isEdit]="false"
[isPreview]="true"
[isMobileDisabled]="true" [isMobileDisabled]="true"
[isEditActionEnabled]="false" [isEditActionEnabled]="false"
[isRemoveActionEnabled]="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() @Input()
isEdit: boolean; isEdit: boolean;
@Input()
isPreview: boolean;
@Input() @Input()
isMobile: boolean; isMobile: boolean;
@ -231,6 +234,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext.store = this.store; this.widgetContext.store = this.store;
this.widgetContext.servicesMap = ServicesMap; this.widgetContext.servicesMap = ServicesMap;
this.widgetContext.isEdit = this.isEdit; this.widgetContext.isEdit = this.isEdit;
this.widgetContext.isPreview = this.isPreview;
this.widgetContext.isMobile = this.isMobile; this.widgetContext.isMobile = this.isMobile;
this.widgetContext.toastTargetId = this.toastTargetId; this.widgetContext.toastTargetId = this.toastTargetId;
@ -252,6 +256,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
handleWidgetAction: this.handleWidgetAction.bind(this), handleWidgetAction: this.handleWidgetAction.bind(this),
elementClick: this.elementClick.bind(this), elementClick: this.elementClick.bind(this),
cardClick: this.cardClick.bind(this), cardClick: this.cardClick.bind(this),
click: this.click.bind(this),
getActiveEntityInfo: this.getActiveEntityInfo.bind(this), getActiveEntityInfo: this.getActiveEntityInfo.bind(this),
openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this), openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this),
openDashboardStateInPopover: this.openDashboardStateInPopover.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.widgetType = this.widgetInfo.widgetTypeFunction;
this.typeParameters = this.widgetInfo.typeParameters; this.typeParameters = this.widgetInfo.typeParameters;
this.widgetContext.embedTitlePanel = this.typeParameters.embedTitlePanel; this.widgetContext.embedTitlePanel = this.typeParameters.embedTitlePanel;
this.widgetContext.overflowVisible = this.typeParameters.overflowVisible;
if (!this.widgetType) { if (!this.widgetType) {
this.widgetTypeInstance = {}; this.widgetTypeInstance = {};
@ -1423,7 +1429,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
} }
private cardClick($event: Event) { 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) { if (descriptors.length) {
$event.stopPropagation(); $event.stopPropagation();
const descriptor = descriptors[0]; 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; height: number;
$scope: IDynamicWidgetComponent; $scope: IDynamicWidgetComponent;
isEdit: boolean; isEdit: boolean;
isPreview: boolean;
isMobile: boolean; isMobile: boolean;
toastTargetId: string; toastTargetId: string;
@ -279,6 +280,7 @@ export class WidgetContext {
timeWindow?: WidgetTimewindow; timeWindow?: WidgetTimewindow;
embedTitlePanel?: boolean; embedTitlePanel?: boolean;
overflowVisible?: boolean;
hideTitlePanel = false; 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, #3347db); // 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, #273cd9); // 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, #2439cd); // 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, #273cd9); // 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-disabled, $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})`);
}
}

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

@ -0,0 +1,271 @@
///
/// 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';
const hoveredFilledDarkenAmount = 6;
const pressedFilledDarkenAmount = 12;
const activatedFilledDarkenAmount = 12;
const pressedRippleFilledDarkenAmount = 18;
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, hoveredFilledDarkenAmount);
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, pressedFilledDarkenAmount);
const mainColorInstance = tinycolor(mainColor);
const mainColorPressedRipple = mainColorInstance.setAlpha(mainColorInstance.getAlpha() * 0.1).toRgbString();
const mainColorPressedRippleFilled = darkenColor(mainColor, pressedRippleFilledDarkenAmount);
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, activatedFilledDarkenAmount);
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);
const brightness = input.getBrightness() / 255;
let ratio: number;
if (brightness >= 0.4 && brightness <= 0.5) {
ratio = brightness + 0.2;
} else {
ratio = Math.max(0.1, Math.log10(brightness * 8));
}
return input.darken(ratio * amount).toRgbString();
};

2
ui-ngx/src/app/shared/components/color-input.component.ts

@ -189,7 +189,7 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
this.popoverService.hidePopover(trigger); this.popoverService.hidePopover(trigger);
} else { } else {
const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer, const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null, this.viewContainerRef, ColorPickerPanelComponent, ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], true, null,
{ {
color: this.colorFormGroup.get('color').value, color: this.colorFormGroup.get('color').value,
colorClearButton: this.colorClearButton colorClearButton: this.colorClearButton

2
ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss

@ -18,9 +18,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 32px; gap: 32px;
overflow: auto;
.saturation-component { .saturation-component {
height: 238px; height: 238px;
min-height: 80px;
border-radius: 8px; border-radius: 8px;
} }

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

@ -15,6 +15,7 @@
/// ///
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { widgetType } from '@shared/models/widget.models';
export enum GetValueAction { export enum GetValueAction {
DO_NOTHING = 'DO_NOTHING', DO_NOTHING = 'DO_NOTHING',
@ -25,6 +26,14 @@ export enum GetValueAction {
export const getValueActions = Object.keys(GetValueAction) as 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>( export const getValueActionTranslations = new Map<GetValueAction, string>(
[ [
[GetValueAction.DO_NOTHING, 'widgets.value-action.do-nothing'], [GetValueAction.DO_NOTHING, 'widgets.value-action.do-nothing'],
@ -45,11 +54,7 @@ export interface TelemetryValueSettings {
key: string; key: string;
} }
export interface GetTelemetryValueSettings extends TelemetryValueSettings { export interface GetAttributeValueSettings extends TelemetryValueSettings {
subscribeForUpdates: boolean;
}
export interface GetAttributeValueSettings extends GetTelemetryValueSettings {
scope: AttributeScope | null; scope: AttributeScope | null;
} }
@ -75,9 +80,9 @@ export interface ValueActionSettings {
export interface GetValueSettings<V> extends ValueActionSettings { export interface GetValueSettings<V> extends ValueActionSettings {
action: GetValueAction; action: GetValueAction;
defaultValue: V; defaultValue: V;
executeRpc: RpcSettings; executeRpc?: RpcSettings;
getAttribute: GetAttributeValueSettings; getAttribute: GetAttributeValueSettings;
getTimeSeries: GetTelemetryValueSettings; getTimeSeries: TelemetryValueSettings;
dataToValue: DataToValueSettings; dataToValue: DataToValueSettings;
} }
@ -89,6 +94,14 @@ export enum SetValueAction {
export const setValueActions = Object.keys(SetValueAction) as 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>( export const setValueActionTranslations = new Map<SetValueAction, string>(
[ [
[SetValueAction.EXECUTE_RPC, 'widgets.value-action.execute-rpc'], [SetValueAction.EXECUTE_RPC, 'widgets.value-action.execute-rpc'],
@ -116,13 +129,3 @@ export interface SetValueSettings extends ValueActionSettings {
putTimeSeries: TelemetryValueSettings; putTimeSeries: TelemetryValueSettings;
valueToData: ValueToDataSettings; valueToData: ValueToDataSettings;
} }
/*export interface RpcStateBehaviourSettings<V> {
initialState: RpcInitialStateSettings<V>;
updateStateByValue: (value: V) => RpcUpdateStateSettings;
}
export interface RpcStateWidgetSettings<V> {
initialState: RpcInitialStateSettings<V>;
background: BackgroundSettings;
}*/

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 { 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 { Injector } from '@angular/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; 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 => { export const getAlarmFilterConfig = (datasources?: Datasource[]): AlarmFilterConfig => {
if (datasources && datasources.length) { if (datasources && datasources.length) {
const config = datasources[0].alarmFilterConfig; const config = datasources[0].alarmFilterConfig;

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

@ -180,6 +180,7 @@ export interface WidgetTypeParameters {
previewWidth?: string; previewWidth?: string;
previewHeight?: string; previewHeight?: string;
embedTitlePanel?: boolean; embedTitlePanel?: boolean;
overflowVisible?: boolean;
hideDataSettings?: boolean; hideDataSettings?: boolean;
defaultDataKeysFunction?: (configComponent: any, configData: any) => DataKey[]; defaultDataKeysFunction?: (configComponent: any, configData: any) => DataKey[];
defaultLatestDataKeysFunction?: (configComponent: any, configData: any) => DataKey[]; defaultLatestDataKeysFunction?: (configComponent: any, configData: any) => DataKey[];
@ -410,6 +411,23 @@ export interface Datasource {
[key: string]: any; [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 { export enum TargetDeviceType {
device = 'device', device = 'device',
entity = 'entity' entity = 'entity'
@ -675,6 +693,14 @@ export const actionDescriptorToAction = (descriptor: WidgetActionDescriptor): Wi
return result; return result;
}; };
export const defaultWidgetAction = (setEntityId = true): WidgetAction => ({
type: WidgetActionType.updateDashboardState,
targetDashboardStateId: null,
openRightLayout: false,
setEntityId,
stateEntityParamName: null
});
export interface WidgetComparisonSettings { export interface WidgetComparisonSettings {
comparisonEnabled?: boolean; comparisonEnabled?: boolean;
timeForComparison?: moment_.unitOfTime.DurationConstructor; 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 { EmbedImageDialogComponent } from '@shared/components/image/embed-image-dialog.component';
import { ImageGalleryDialogComponent } from '@shared/components/image/image-gallery-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 { 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) { export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService; return markedOptionsService;
@ -414,7 +415,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GalleryImageInputComponent, GalleryImageInputComponent,
MultipleGalleryImageInputComponent, MultipleGalleryImageInputComponent,
EmbedImageDialogComponent, EmbedImageDialogComponent,
ImageGalleryDialogComponent ImageGalleryDialogComponent,
WidgetButtonComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -666,7 +668,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GalleryImageInputComponent, GalleryImageInputComponent,
MultipleGalleryImageInputComponent, MultipleGalleryImageInputComponent,
EmbedImageDialogComponent, EmbedImageDialogComponent,
ImageGalleryDialogComponent ImageGalleryDialogComponent,
WidgetButtonComponent
] ]
}) })
export class SharedModule { } export class SharedModule { }

50
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." "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure."
}, },
"widgets": { "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", "background": "Background",
"background-settings": "Background settings", "background-settings": "Background settings",
@ -6483,15 +6519,20 @@
"source-entity-alias": "Source entity alias", "source-entity-alias": "Source entity alias",
"source-entity-attribute": "Source entity attribute" "source-entity-attribute": "Source entity attribute"
}, },
"value-action": { "rpc-state": {
"initial-state": "Initial state", "initial-state": "Initial state",
"initial-state-hint": "Action to get the initial value of the component.", "initial-state-hint": "Action to get the initial value of the component.",
"disabled-state": "Disabled state",
"disabled-state-hint": "Condition under which the component is disabled.",
"turn-on": "Turn 'On'", "turn-on": "Turn 'On'",
"turn-on-hint": "Action performed to turn ON the component.", "turn-on-hint": "Action performed to turn ON the component.",
"turn-off": "Turn 'Off'", "turn-off": "Turn 'Off'",
"turn-off-hint": "Action performed to turn OFF the component.", "turn-off-hint": "Action performed to turn OFF the component.",
"on": "On", "on": "On",
"off": "Off", "off": "Off",
"disabled": "Disabled"
},
"value-action": {
"do-nothing": "Do nothing", "do-nothing": "Do nothing",
"execute-rpc": "Execute RPC", "execute-rpc": "Execute RPC",
"get-attribute": "Get attribute", "get-attribute": "Get attribute",
@ -6523,14 +6564,12 @@
"attribute-key-required": "Attribute key is required.", "attribute-key-required": "Attribute key is required.",
"time-series-key": "Time-series key", "time-series-key": "Time-series key",
"time-series-key-required": "Time-series key is required.", "time-series-key-required": "Time-series key is required.",
"subscribe-for-updates": "Subscribe for updates",
"subscribe-for-updates-hint": "Subscribe for updates",
"action-result-converter": "Action result converter", "action-result-converter": "Action result converter",
"converter-none": "None", "converter-none": "None",
"converter-function": "Function", "converter-function": "Function",
"converter-constant": "Constant", "converter-constant": "Constant",
"parse-value-function": "Parse value function", "parse-value-function": "Parse value function",
"on-when-result-is": "'On' when result is", "state-when-result-is": "'{{state}}' when result is",
"parameters": "Parameters", "parameters": "Parameters",
"convert-value-function": "Convert value function", "convert-value-function": "Convert value function",
"error": { "error": {
@ -6785,7 +6824,8 @@
"element-click": "On HTML element click", "element-click": "On HTML element click",
"pie-slice-click": "On slice click", "pie-slice-click": "On slice click",
"row-double-click": "On row double click", "row-double-click": "On row double click",
"card-click": "On card click" "card-click": "On card click",
"click": "On click"
} }
}, },
"paginator" : { "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-chip-listbox.center-stretch {
.mat-mdc-standard-chip { .mat-mdc-standard-chip {
flex: 1; flex: 1;

Loading…
Cancel
Save