From f9d2549c2c901775edc8b074a352aa6ca478551e Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 7 Nov 2024 15:23:35 +0100 Subject: [PATCH 01/10] js-executor: added request body on debug level --- msa/js-executor/api/jsInvokeMessageProcessor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/msa/js-executor/api/jsInvokeMessageProcessor.ts b/msa/js-executor/api/jsInvokeMessageProcessor.ts index 98b9c0fe0c..fe18c320bc 100644 --- a/msa/js-executor/api/jsInvokeMessageProcessor.ts +++ b/msa/js-executor/api/jsInvokeMessageProcessor.ts @@ -130,7 +130,7 @@ export class JsInvokeMessageProcessor { processCompileRequest(requestId: string, responseTopic: string, headers: any, compileRequest: JsCompileRequest) { const scriptId = JsInvokeMessageProcessor.getScriptId(compileRequest); - this.logger.debug('[%s] Processing compile request, scriptId: [%s]', requestId, scriptId); + this.logger.debug('[%s] Processing compile request, scriptId: [%s], compileRequest [%s]', requestId, scriptId, compileRequest); if (this.scriptMap.has(scriptId)) { const compileResponse = JsInvokeMessageProcessor.createCompileResponse(scriptId, true); this.logger.debug('[%s] Script was already compiled, scriptId: [%s]', requestId, scriptId); @@ -154,7 +154,7 @@ export class JsInvokeMessageProcessor { processInvokeRequest(requestId: string, responseTopic: string, headers: any, invokeRequest: JsInvokeRequest) { const scriptId = JsInvokeMessageProcessor.getScriptId(invokeRequest); - this.logger.debug('[%s] Processing invoke request, scriptId: [%s]', requestId, scriptId); + this.logger.debug('[%s] Processing invoke request, scriptId: [%s], invokeRequest [%s]', requestId, scriptId, invokeRequest); this.executedScriptsCounter++; if (this.executedScriptsCounter % statFrequency == 0) { const nowMs = performance.now(); @@ -217,7 +217,7 @@ export class JsInvokeMessageProcessor { processReleaseRequest(requestId: string, responseTopic: string, headers: any, releaseRequest: JsReleaseRequest) { const scriptId = JsInvokeMessageProcessor.getScriptId(releaseRequest); - this.logger.debug('[%s] Processing release request, scriptId: [%s]', requestId, scriptId); + this.logger.debug('[%s] Processing release request, scriptId: [%s], releaseRequest [%s]', requestId, scriptId, releaseRequest); if (this.scriptMap.has(scriptId)) { const index = this.scriptIds.indexOf(scriptId); if (index > -1) { From 53da8cd47e3df424484463f057a47b1d316f7599 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 28 Nov 2024 18:55:01 +0200 Subject: [PATCH 02/10] UI: Segmented button --- .../json/system/widget_bundles/buttons.json | 1 + .../widget_bundles/control_widgets.json | 1 + .../system/widget_types/segmented_button.json | 43 +++ .../basic/basic-widget-config.module.ts | 6 + ...gmented-button-basic-config.component.html | 271 ++++++++++++++ ...segmented-button-basic-config.component.ts | 145 ++++++++ .../segmented-button-widget.component.html | 33 ++ .../segmented-button-widget.component.scss | 37 ++ .../segmented-button-widget.component.ts | 118 ++++++ .../button/segmented-button-widget.models.ts | 346 ++++++++++++++++++ ...nted-button-widget-settings.component.html | 263 +++++++++++++ ...mented-button-widget-settings.component.ts | 134 +++++++ ...n-toggle-custom-style-panel.component.html | 92 +++++ ...n-toggle-custom-style-panel.component.scss | 68 ++++ ...ton-toggle-custom-style-panel.component.ts | 210 +++++++++++ ...-button-toggle-custom-style.component.html | 44 +++ ...-button-toggle-custom-style.component.scss | 46 +++ ...et-button-toggle-custom-style.component.ts | 169 +++++++++ .../common/widget-settings-common.module.ts | 10 + .../lib/settings/widget-settings.module.ts | 6 + .../widget/widget-components.module.ts | 3 + .../widget-button-toggle.component.html | 42 +++ .../widget-button-toggle.component.scss | 189 ++++++++++ .../button/widget-button-toggle.component.ts | 234 ++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 27 +- .../segmented-button/rounded-layout.svg | 1 + .../segmented-button/squared-layout.svg | 1 + 28 files changed, 2541 insertions(+), 2 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/segmented_button.json create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts create mode 100644 ui-ngx/src/app/shared/components/button/widget-button-toggle.component.html create mode 100644 ui-ngx/src/app/shared/components/button/widget-button-toggle.component.scss create mode 100644 ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts create mode 100644 ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg create mode 100644 ui-ngx/src/assets/widget/segmented-button/squared-layout.svg diff --git a/application/src/main/data/json/system/widget_bundles/buttons.json b/application/src/main/data/json/system/widget_bundles/buttons.json index 115db85e66..aaed9f73e1 100644 --- a/application/src/main/data/json/system/widget_bundles/buttons.json +++ b/application/src/main/data/json/system/widget_bundles/buttons.json @@ -11,6 +11,7 @@ "action_button", "command_button", "toggle_button", + "segmented_button", "power_button" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index f61219bfe6..6bc4bdc830 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -11,6 +11,7 @@ "single_switch", "command_button", "toggle_button", + "segmented_button", "power_button", "slider", "control_widgets.switch_control", diff --git a/application/src/main/data/json/system/widget_types/segmented_button.json b/application/src/main/data/json/system/widget_types/segmented_button.json new file mode 100644 index 0000000000..55e1165b83 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/segmented_button.json @@ -0,0 +1,43 @@ +{ + "fqn": "segmented_button", + "name": "Segmented button", + "deprecated": false, + "image": "tb-image;/api/images/system/segmented-button.svg", + "description": "Facilitates navigation to other dashboards, states, or custom actions. Configurable settings allow for on-click action definition and conditions for button activation or deactivation. It offers various layouts and custom styling options for different stat", + "descriptor": { + "type": "latest", + "sizeX": 5, + "sizeY": 1.5, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n maxDatasources: 1,\n maxDataKeys: 0,\n singleEntity: true,\n previewWidth: '300px',\n previewHeight: '80px',\n embedTitlePanel: true,\n overflowVisible: true,\n hideDataSettings: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "{}\n", + "settingsDirective": "tb-segmented-button-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-segmented-button-basic-config", + "defaultConfig": "{\"targetDeviceAliases\":[],\"showTitle\":false,\"backgroundColor\":\"#E8E8E8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"initialState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":true,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"},\"executeRpc\":{\"method\":null,\"requestTimeout\":null,\"requestPersistent\":null,\"persistentPollingInterval\":null}},\"leftButtonClick\":{\"type\":\"updateDashboardState\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null},\"rightButtonClick\":{\"type\":\"updateDashboardState\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null},\"disabledState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":false,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"}},\"appearance\":{\"layout\":\"squared\",\"autoScale\":true,\"cardBorder\":1,\"cardBorderColor\":\"#305680\",\"leftAppearance\":{\"showLabel\":true,\"label\":\"Traditional\",\"labelFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":14,\"sizeUnit\":\"px\",\"lineHeight\":\"18px\"},\"showIcon\":true,\"icon\":\"rocket_launch\",\"iconSize\":24,\"iconSizeUnit\":\"px\"},\"rightAppearance\":{\"showLabel\":true,\"label\":\"Hi-Perf\",\"labelFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":14,\"sizeUnit\":\"px\",\"lineHeight\":\"18px\"},\"showIcon\":true,\"icon\":\"rocket_launch\",\"iconSize\":24,\"iconSizeUnit\":\"px\"},\"selectedStyle\":{\"mainColor\":\"#FFFFFF\",\"backgroundColor\":\"#305680\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"disabled\":null}},\"unselectedStyle\":{\"mainColor\":\"#000000C2\",\"backgroundColor\":\"#E8E8E8\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"disabled\":null}}}},\"title\":\"Segmented button\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\",\"datasources\":[],\"actions\":{\"click\":[{\"id\":\"56ebc389-f91d-fc25-36ef-0d18329fbeda\",\"name\":\"onClick\",\"icon\":\"more_horiz\",\"type\":\"doNothing\",\"targetDashboardStateId\":null,\"openRightLayout\":false,\"setEntityId\":true,\"stateEntityParamName\":null,\"openInSeparateDialog\":false,\"openInPopover\":false}]},\"borderRadius\":\"4px\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}}}" + }, + "resources": [ + { + "link": "/api/images/system/segmented-button.svg", + "title": "\"Action button\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "segmented-button.svg", + "publicResourceKey": "koboT6A2CqNyRYPTP3ENmjMM6P8KK1PP", + "mediaType": "image/svg+xml", + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgZmlsbD0ibm9uZSIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPHJlY3QgeD0iLjM4IiB5PSI1OC4yIiB3aWR0aD0iMTk5LjI1IiBoZWlnaHQ9IjQzLjYxIiByeD0iMi42MyIgc3Ryb2tlPSIjMzA1NjgwIiBzdHJva2Utd2lkdGg9Ii43NSIvPgogPHJlY3QgeD0iMy4wMSIgeT0iNjAuODMiIHdpZHRoPSI5Ni45OSIgaGVpZ2h0PSIzOC4zNSIgcng9IjEuNSIgZmlsbD0iIzMwNTY4MCIvPgogPHBhdGggZD0iTTI5LjI4IDc1LjUyVjgzSDI4di03LjQ4aDEuMjh6bTIuMzUgMHYxLjAyaC01Ljk2di0xLjAyaDUuOTZ6bTEuOTIgMi45OFY4M2gtMS4yM3YtNS41NmgxLjE4bC4wNSAxLjA2em0xLjctMS4xdjEuMTVhMi40NCAyLjQ0IDAgMCAwLTEuMDcuMDYgMS4wNiAxLjA2IDAgMCAwLS42NS42NWMtLjA2LjE2LS4xLjM0LS4xLjUzbC0uMjkuMDJjMC0uMzUuMDQtLjY3LjEtLjk3LjA3LS4zLjE4LS41Ni4zMS0uNzguMTQtLjIzLjMyLS40LjUzLS41M2ExLjQgMS40IDAgMCAxIC45OC0uMTdsLjIuMDR6bTMuOTIgNC40OHYtMi42NWMwLS4yLS4wNC0uMzctLjEtLjUxYS43Ni43NiAwIDAgMC0uMzQtLjM0IDEuMTIgMS4xMiAwIDAgMC0uNTQtLjExYy0uMiAwLS4zOC4wMy0uNTMuMWEuODUuODUgMCAwIDAtLjM0LjI4LjY3LjY3IDAgMCAwLS4xMi40aC0xLjI0YzAtLjIzLjA2LS40NC4xNi0uNjUuMS0uMi4yNi0uMzguNDYtLjU1LjItLjE2LjQ1LS4yOC43My0uMzguMjgtLjA5LjYtLjEzLjk0LS4xMy40MiAwIC44LjA3IDEuMTIuMi4zMi4xNS41OC4zNi43Ni42NC4xOS4yOC4yOC42NC4yOCAxLjA2djIuNDdjMCAuMjYuMDIuNDkuMDYuNjkuMDMuMi4wOS4zNy4xNS41MlY4M2gtMS4yN2EyLjIgMi4yIDAgMCAxLS4xMy0uNWMtLjAzLS4yMi0uMDUtLjQyLS4wNS0uNjJ6bS4xOC0yLjI2LjAxLjc2aC0uODljLS4yMyAwLS40My4wMy0uNi4wNy0uMTguMDQtLjMzLjEtLjQ0LjE5cy0uMi4xOC0uMjYuM2EuODcuODcgMCAwIDAtLjEuMzljMCAuMTUuMDQuMjguMTEuNC4wNy4xMy4xNy4yMi4zLjI5LjEzLjA3LjMuMS40OC4xYTEuMzYgMS4zNiAwIDAgMCAxLjEyLS41NGMuMS0uMTUuMTctLjMuMTctLjQ0bC40LjU1Yy0uMDQuMTQtLjEuMy0uMi40Ni0uMS4xNi0uMjQuMzEtLjQuNDZhMS45NCAxLjk0IDAgMCAxLTEuMzMuNWMtLjM2IDAtLjY5LS4wOC0uOTctLjIyLS4yOS0uMTUtLjUtLjM1LS42Ny0uNmExLjc3IDEuNzcgMCAwIDEtLjA4LTEuNjNjLjEyLS4yMi4yOC0uNDIuNS0uNTcuMjItLjE1LjQ4LS4yNy44LS4zNS4zMS0uMDguNjctLjEyIDEuMDgtLjEyaC45N3ptNS45NCAyLjIzVjc1LjFoMS4yNFY4M2gtMS4xMmwtLjEyLTEuMTV6bS0zLjYyLTEuNTd2LS4xYzAtLjQzLjA1LS44MS4xNS0xLjE2LjEtLjM1LjI0LS42NS40My0uOWExLjkgMS45IDAgMCAxIDEuNi0uNzggMS44NCAxLjg0IDAgMCAxIDEuNTMuNzZjLjE4LjIzLjMzLjUyLjQzLjg1LjExLjM0LjE4LjcuMjMgMS4xMXYuMzVjLS4wNS40LS4xMi43Ni0uMjMgMS4wOS0uMS4zMy0uMjUuNjEtLjQyLjg1YTEuODQgMS44NCAwIDAgMS0xLjU1Ljc1IDEuOTUgMS45NSAwIDAgMS0xLjU5LS44IDIuNzkgMi43OSAwIDAgMS0uNDMtLjljLS4xLS4zNC0uMTUtLjcyLS4xNS0xLjEyem0xLjI0LS4xdi4xYzAgLjI1LjAyLjUuMDcuNzEuMDQuMjIuMTIuNDEuMjIuNTguMS4xNy4yMi4zLjM4LjQuMTYuMDguMzYuMTMuNTguMTMuMjggMCAuNTEtLjA2LjctLjE4cy4zMi0uMy40Mi0uNWMuMS0uMjIuMTgtLjQ1LjIxLS43MXYtLjkzYy0uMDItLjItLjA2LS40LS4xMi0uNTdhMS41MSAxLjUxIDAgMCAwLS4yNi0uNDYgMS4yNCAxLjI0IDAgMCAwLS45NS0uNDJjLS4yMiAwLS40MS4wNS0uNTcuMTUtLjE1LjEtLjI4LjIyLS4zOS40LS4xLjE2LS4xNy4zNi0uMjIuNThzLS4wNy40Ni0uMDcuNzF6bTYuNDMtMi43NFY4M0g0OC4xdi01LjU2aDEuMjR6bS0xLjMyLTEuNDZjMC0uMTkuMDYtLjM1LjE4LS40N2EuNy43IDAgMCAxIC41My0uMTljLjIxIDAgLjM5LjA2LjUxLjIuMTMuMTEuMi4yNy4yLjQ2IDAgLjE4LS4wNy4zNC0uMi40NmEuNzEuNzEgMCAwIDEtLjUxLjE5LjcyLjcyIDAgMCAxLS41My0uMTkuNjMuNjMgMCAwIDEtLjE4LS40NnptNS40MiAxLjQ2di45SDUwLjN2LS45aDMuMTR6bS0yLjIzLTEuMzZoMS4yM3Y1LjM4YzAgLjE3LjAzLjMuMDguNC4wNS4wOS4xMi4xNS4yLjE4LjEuMDMuMi4wNC4zMi4wNGExLjkgMS45IDAgMCAwIC40NC0uMDR2Ljk0YTMuMTcgMy4xNyAwIDAgMS0uODIuMTJjLS4yOCAwLS41NC0uMDUtLjc2LS4xNS0uMjEtLjEtLjM5LS4yNy0uNS0uNWExLjk0IDEuOTQgMCAwIDEtLjItLjkxdi01LjQ2em00LjcgMS4zNlY4M2gtMS4yNXYtNS41NmgxLjI1em0tMS4zMy0xLjQ2YzAtLjE5LjA2LS4zNS4xOS0uNDdhLjcuNyAwIDAgMSAuNTItLjE5Yy4yMiAwIC40LjA2LjUyLjIuMTMuMTEuMTkuMjcuMTkuNDYgMCAuMTgtLjA2LjM0LS4yLjQ2YS43MS43MSAwIDAgMS0uNTEuMTkuNzIuNzIgMCAwIDEtLjUyLS4xOS42My42MyAwIDAgMS0uMTktLjQ2em0yLjYzIDQuM3YtLjEyYzAtLjQuMDYtLjc3LjE4LTEuMTEuMTItLjM1LjI4LS42NS41LS45LjIzLS4yNi41LS40Ni44Mi0uNi4zMi0uMTQuNjgtLjIxIDEuMDktLjIxLjQgMCAuNzcuMDcgMS4wOC4yMS4zMy4xNC42LjM0LjgyLjYuMjIuMjUuNC41NS41MS45LjEyLjM0LjE4LjcxLjE4IDEuMTF2LjEyYzAgLjQtLjA2Ljc3LS4xOCAxLjEyYTIuMzYgMi4zNiAwIDAgMS0yLjQgMS43Yy0uNDEgMC0uNzgtLjA3LTEuMS0uMmEyLjM2IDIuMzYgMCAwIDEtLjgxLS42Yy0uMjItLjI2LS40LS41Ni0uNTEtLjlhMy40NCAzLjQ0IDAgMCAxLS4xOC0xLjEyem0xLjI0LS4xMnYuMTJjMCAuMjUuMDMuNDkuMDguNzEuMDUuMjIuMTMuNDIuMjQuNTkuMTEuMTYuMjUuMy40Mi40LjE4LjA5LjM4LjE0LjYyLjE0YTEuMTggMS4xOCAwIDAgMCAxLjAxLS41NGMuMTEtLjE3LjItLjM3LjI0LS41OS4wNi0uMjIuMDktLjQ2LjA5LS43di0uMTNjMC0uMjQtLjAzLS40OC0uMDktLjdhMS44IDEuOCAwIDAgMC0uMjQtLjU5IDEuMTggMS4xOCAwIDAgMC0xLjAyLS41NWMtLjI0IDAtLjQ0LjA1LS42MS4xNS0uMTcuMS0uMy4yMy0uNDIuNC0uMS4xNy0uMTkuMzctLjI0LjYtLjA1LjIxLS4wOC40NS0uMDguN3ptNi4zOS0xLjUzVjgzSDYzLjZ2LTUuNTZoMS4xN2wuMDcgMS4xOXpNNjQuNjIgODBoLS40YzAtLjQuMDYtLjc2LjE2LTEuMDkuMTEtLjMzLjI2LS42LjQ2LS44NGEyIDIgMCAwIDEgMS42LS43NGMuMjcgMCAuNS4wMy43Mi4xLjIyLjA4LjQuMi41Ni4zNi4xNi4xNy4yOC4zOC4zNi42NS4wOS4yNi4xMy41OC4xMy45NlY4M2gtMS4yNXYtMy42YzAtLjI3LS4wNC0uNDgtLjEyLS42M2EuNjYuNjYgMCAwIDAtLjMzLS4zM2MtLjE0LS4wNy0uMzItLjEtLjU0LS4xYTEuMiAxLjIgMCAwIDAtMSAuNWMtLjEuMTUtLjIuMzItLjI2LjUzLS4wNi4yLS4xLjQxLS4xLjY0em04LjE3IDEuODd2LTIuNjVjMC0uMi0uMDMtLjM3LS4xLS41MWEuNzYuNzYgMCAwIDAtLjMzLS4zNCAxLjEyIDEuMTIgMCAwIDAtLjU1LS4xMWMtLjIgMC0uMzcuMDMtLjUyLjFhLjg1Ljg1IDAgMCAwLS4zNS4yOC42Ny42NyAwIDAgMC0uMTIuNEg2OS42YzAtLjIzLjA1LS40NC4xNi0uNjUuMS0uMi4yNi0uMzguNDYtLjU1LjItLjE2LjQ0LS4yOC43Mi0uMzguMjgtLjA5LjYtLjEzLjk1LS4xMy40MiAwIC43OS4wNyAxLjExLjIuMzMuMTUuNTguMzYuNzcuNjQuMTguMjguMjguNjQuMjggMS4wNnYyLjQ3YzAgLjI2LjAyLjQ5LjA1LjY5LjA0LjIuMDkuMzcuMTYuNTJWODNoLTEuMjdhMi4yIDIuMiAwIDAgMS0uMTQtLjVjLS4wMy0uMjItLjA1LS40Mi0uMDUtLjYyem0uMTgtMi4yNi4wMS43NmgtLjg4Yy0uMjMgMC0uNDQuMDMtLjYxLjA3LS4xOC4wNC0uMzIuMS0uNDQuMTlzLS4yLjE4LS4yNi4zYS44Ny44NyAwIDAgMC0uMDkuMzljMCAuMTUuMDQuMjguMS40LjA3LjEzLjE3LjIyLjMuMjkuMTQuMDcuMy4xLjQ5LjFhMS4zNiAxLjM2IDAgMCAwIDEuMTEtLjU0Yy4xMS0uMTUuMTctLjMuMTgtLjQ0bC40LjU1YTIuMiAyLjIgMCAwIDEtLjYuOTEgMS45NCAxLjk0IDAgMCAxLTEuMzQuNWMtLjM2IDAtLjY4LS4wNy0uOTctLjIxLS4yOC0uMTUtLjUtLjM1LS42Ni0uNmExLjc3IDEuNzcgMCAwIDEtLjA4LTEuNjNjLjExLS4yMi4yOC0uNDIuNS0uNTcuMjEtLjE1LjQ4LS4yNy44LS4zNS4zLS4wOC42Ny0uMTIgMS4wNy0uMTJoLjk3em0zLjg4LTQuNTJWODNINzUuNnYtNy45aDEuMjV6IiBmaWxsPSIjZmZmIi8+CiA8cGF0aCBkPSJNMTM2Ljg2IDc4LjY0djEuMDJoLTMuOTd2LTEuMDJoMy45N3ptLTMuNjUtMy4xMlY4M2gtMS4zdi03LjQ4aDEuM3ptNC42NCAwVjgzaC0xLjI4di03LjQ4aDEuMjh6bTIuOTMgMS45MlY4M2gtMS4yNHYtNS41NmgxLjI0em0tMS4zMy0xLjQ2YzAtLjE5LjA3LS4zNS4xOS0uNDcuMTMtLjEzLjMtLjE5LjUyLS4xOS4yMiAwIC40LjA2LjUyLjIuMTMuMTEuMi4yNy4yLjQ2IDAgLjE4LS4wNy4zNC0uMi40NmEuNzEuNzEgMCAwIDEtLjUyLjE5LjcyLjcyIDAgMCAxLS41Mi0uMTkuNjMuNjMgMCAwIDEtLjE5LS40NnptNS4zNyAzLjMzdi45OWgtMi43MnYtMWgyLjcyem00LjI0LjloLTEuOTV2LTEuMDJoMS45NGMuMzQgMCAuNjItLjA2LjgzLS4xNy4yLS4xLjM2LS4yNi40Ni0uNDUuMS0uMi4xNC0uNDIuMTQtLjY3IDAtLjI0LS4wNS0uNDYtLjE0LS42Ni0uMS0uMjEtLjI1LS4zOC0uNDYtLjVzLS40OS0uMi0uODItLjJoLTEuNTZWODNoLTEuMjl2LTcuNDhoMi44NWMuNTcgMCAxLjA3LjEgMS40Ny4zLjQuMi43Mi40OS45My44NS4yMS4zNS4zMi43Ni4zMiAxLjIyIDAgLjQ4LS4xLjktLjMyIDEuMjQtLjIxLjM1LS41Mi42Mi0uOTMuOC0uNC4xOS0uOS4yOC0xLjQ3LjI4em02LjMyIDIuOWMtLjQxIDAtLjc4LS4wNy0xLjExLS4yYTIuNDYgMi40NiAwIDAgMS0xLjM4LTEuNDQgMy4wMSAzLjAxIDAgMCAxLS4xOC0xLjA2di0uMmMwLS40NC4wNi0uODQuMTktMS4xOS4xMi0uMzUuMy0uNjUuNTMtLjkuMjItLjI2LjQ5LS40NS44LS41OC4zLS4xNC42NC0uMiAxLS4yLjQgMCAuNzUuMDYgMS4wNC4yLjMuMTMuNTUuMzIuNzQuNTYuMi4yNC4zNS41My40NS44Ni4xLjMzLjE1LjcuMTUgMS4xdi41M2gtNC4zdi0uODloMy4wN3YtLjFjMC0uMjItLjA1LS40My0uMTMtLjYyLS4wOC0uMi0uMi0uMzYtLjM3LS40OGExLjEgMS4xIDAgMCAwLS42NS0uMThjLS4yMSAwLS40LjA1LS41NS4xNC0uMTYuMDgtLjMuMi0uNC4zN3MtLjE5LjM2LS4yNS42YTMuNCAzLjQgMCAwIDAtLjA4Ljc3di4yYzAgLjI1LjAzLjQ4LjEuNjkuMDcuMi4xNy4zOC4zLjU0YTEuMzQgMS4zNCAwIDAgMCAxLjEuNWMuMyAwIC41Ny0uMDcuOC0uMTlzLjQzLS4yOS42LS41bC42Ni42MmEyLjM0IDIuMzQgMCAwIDEtMS4xNy44OWMtLjI4LjEtLjYuMTUtLjk2LjE1ek0xNjAgNzguNVY4M2gtMS4yNHYtNS41NmgxLjE5bC4wNSAxLjA2em0xLjctMS4xdjEuMTVhMi40NyAyLjQ3IDAgMCAwLTEuMDcuMDYgMS4wNSAxLjA1IDAgMCAwLS42NS42NWMtLjA2LjE2LS4xLjM0LS4xLjUzbC0uMjkuMDJjMC0uMzUuMDQtLjY3LjEtLjk3LjA3LS4zLjE4LS41Ni4zMS0uNzguMTQtLjIzLjMyLS40LjUzLS41M2ExLjQgMS40IDAgMCAxIC45Ny0uMTdsLjIuMDR6bTIuNzQgNS42aC0xLjI0di02LjFjMC0uNDEuMDgtLjc2LjIzLTEuMDQuMTYtLjI4LjM4LS41LjY3LS42NGEyLjU4IDIuNTggMCAwIDEgMS43NS0uMTNsLS4wMy45NmExLjA0IDEuMDQgMCAwIDAtLjk3LjA2LjcyLjcyIDAgMCAwLS4zMS4zYy0uMDcuMTMtLjEuMy0uMS41Vjgzem0xLjE0LTUuNTZ2LjloLTMuMjR2LS45aDMuMjR6IiBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9Ii43NiIvPgo8L3N2Zz4K", + "public": true + } + ], + "scada": false, + "tags": [ + "button", + "action", + "navigation", + "navigate", + "dashboard state" + ] +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index a32d7c2311..800deec7e5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -143,6 +143,9 @@ import { UnreadNotificationBasicConfigComponent } from '@home/components/widget/config/basic/cards/unread-notification-basic-config.component'; import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/basic/scada/scada-symbol-basic-config.component'; +import { + SegmentedButtonBasicConfigComponent +} from '@home/components/widget/config/basic/button/segmented-button-basic-config.component'; @NgModule({ declarations: [ @@ -174,6 +177,7 @@ import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/ BarChartWithLabelsBasicConfigComponent, SingleSwitchBasicConfigComponent, ActionButtonBasicConfigComponent, + SegmentedButtonBasicConfigComponent, CommandButtonBasicConfigComponent, PowerButtonBasicConfigComponent, SliderBasicConfigComponent, @@ -227,6 +231,7 @@ import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/ BarChartWithLabelsBasicConfigComponent, SingleSwitchBasicConfigComponent, ActionButtonBasicConfigComponent, + SegmentedButtonBasicConfigComponent, CommandButtonBasicConfigComponent, PowerButtonBasicConfigComponent, SliderBasicConfigComponent, @@ -272,6 +277,7 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type + + + +
+
widgets.action-button.behavior
+
+
widgets.button-state.initial-state
+ +
+ +
+
widgets.action-button.left-button-click
+ + +
+
+
widgets.action-button.right-button-click
+ + +
+ +
+
widgets.button-state.disabled-state
+ +
+
+
+
widget-config.appearance
+ + + + {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} + + +
+ + {{ 'widgets.button.auto-scale' | translate }} + +
+
+
widgets.segmented-button.card-border
+
+ + +
px
+
+ + +
+
+
+ +
+
+
widgets.segmented-button.button-appearance
+ + {{ 'widgets.segmented-button.left' | translate }} + {{ 'widgets.segmented-button.right' | translate }} + +
+
+
+ + {{ 'widgets.button.label' | translate }} + +
+ + + + + +
+
+
+ + {{ 'widgets.button.icon' | translate }} + +
+ + + + + + +
+
+
+ +
+
+ + {{ 'widgets.button.label' | translate }} + +
+ + + + + +
+
+
+ + {{ 'widgets.button.icon' | translate }} + +
+ + + + + + +
+
+
+
+ +
+
+
widgets.segmented-button.color-styles
+ + {{ 'widgets.segmented-button.selected' | translate }} + {{ 'widgets.segmented-button.unselected' | translate }} + +
+
+
+
{{ 'widgets.button.color-palette' | translate }}
+
+
+
widgets.button.main
+ + +
+ +
+
widgets.button.background
+ + +
+
+
+
+ + + +
widgets.button.custom-styles
+
+
+ +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+
+
+
+
+
+
+
{{ 'widgets.button.color-palette' | translate }}
+
+
+
widgets.button.main
+ + +
+ +
+
widgets.button.background
+ + +
+
+
+
+ + + +
widgets.button.custom-styles
+
+
+ +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts new file mode 100644 index 0000000000..2b86d88a4a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts @@ -0,0 +1,145 @@ +/// +/// 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 { Datasource, TargetDevice, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { ValueType } from '@shared/models/constants'; +import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models'; +import { + SegmentedButtonAppearanceType, + SegmentedButtonColorStylesType, + segmentedButtonDefaultSettings, + segmentedButtonLayoutBorder, + segmentedButtonLayoutImages, + segmentedButtonLayouts, + segmentedButtonLayoutTranslations, + SegmentedButtonWidgetSettings, + WidgetButtonToggleState, + widgetButtonToggleStatesTranslations +} from '@home/components/widget/lib/button/segmented-button-widget.models'; + +@Component({ + selector: 'tb-segmented-button-basic-config', + templateUrl: './segmented-button-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class SegmentedButtonBasicConfigComponent extends BasicWidgetConfigComponent { + + get targetDevice(): TargetDevice { + const datasources: Datasource[] = this.segmentedButtonWidgetConfigForm.get('datasources').value; + return getTargetDeviceFromDatasources(datasources); + } + + segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'left'; + segmentedButtonColorStylesType: SegmentedButtonColorStylesType = 'selected'; + + widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[]; + widgetButtonToggleStatesTranslationsMap = widgetButtonToggleStatesTranslations; + + segmentedButtonLayouts = segmentedButtonLayouts; + segmentedButtonLayoutTranslationMap = segmentedButtonLayoutTranslations; + segmentedButtonLayoutImageMap = segmentedButtonLayoutImages; + segmentedButtonLayoutBorderMap = segmentedButtonLayoutBorder; + + valueType = ValueType; + + segmentedButtonWidgetConfigForm: UntypedFormGroup; + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.segmentedButtonWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: SegmentedButtonWidgetSettings = {...segmentedButtonDefaultSettings, ...(configData.config.settings || {})}; + + this.segmentedButtonWidgetConfigForm = this.fb.group({ + datasources: [configData.config.datasources, []], + + initialState: [settings.initialState, []], + leftButtonClick: [settings.leftButtonClick, []], + rightButtonClick: [settings.rightButtonClick, []], + disabledState: [settings.disabledState, []], + + appearance: this.fb.group({ + layout: [settings.appearance.layout, []], + autoScale: [settings.appearance.autoScale, []], + cardBorder: [settings.appearance.cardBorder, []], + cardBorderColor: [settings.appearance.cardBorderColor, []], + leftAppearance: this.fb.group({ + showLabel: [settings.appearance.leftAppearance.showLabel, []], + label: [settings.appearance.leftAppearance.label, []], + labelFont: [settings.appearance.leftAppearance.labelFont, []], + showIcon: [settings.appearance.leftAppearance.showIcon, []], + icon: [settings.appearance.leftAppearance.icon, []], + iconSize: [settings.appearance.leftAppearance.iconSize, []], + iconSizeUnit: [settings.appearance.leftAppearance.iconSizeUnit, []], + }), + rightAppearance: this.fb.group({ + showLabel: [settings.appearance.rightAppearance.showLabel, []], + label: [settings.appearance.rightAppearance.label, []], + labelFont: [settings.appearance.rightAppearance.labelFont, []], + showIcon: [settings.appearance.rightAppearance.showIcon, []], + icon: [settings.appearance.rightAppearance.icon, []], + iconSize: [settings.appearance.rightAppearance.iconSize, []], + iconSizeUnit: [settings.appearance.rightAppearance.iconSizeUnit, []], + }), + selectedStyle: this.fb.group({ + mainColor: [settings.appearance.selectedStyle.mainColor, []], + backgroundColor: [settings.appearance.selectedStyle.backgroundColor, []], + customStyle: this.fb.group({ + enabled: [settings.appearance.selectedStyle.customStyle.enabled, []], + hovered: [settings.appearance.selectedStyle.customStyle.hovered, []], + disabled: [settings.appearance.selectedStyle.customStyle.disabled, []], + }) + }), + unselectedStyle: this.fb.group({ + mainColor: [settings.appearance.unselectedStyle.mainColor, []], + backgroundColor: [settings.appearance.unselectedStyle.backgroundColor, []], + customStyle: this.fb.group({ + enabled: [settings.appearance.unselectedStyle.customStyle.enabled, []], + hovered: [settings.appearance.unselectedStyle.customStyle.hovered, []], + disabled: [settings.appearance.unselectedStyle.customStyle.disabled, []], + }) + }), + }) + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.datasources = config.datasources; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.initialState = config.initialState; + this.widgetConfig.config.settings.disabledState = config.disabledState; + this.widgetConfig.config.settings.leftButtonClick = config.leftButtonClick; + this.widgetConfig.config.settings.rightButtonClick = config.rightButtonClick; + this.widgetConfig.config.settings.appearance = config.appearance; + this.widgetConfig.config.borderRadius = this.segmentedButtonLayoutBorderMap.get(config.appearance.layout); + return this.widgetConfig; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.html new file mode 100644 index 0000000000..10cf016e11 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.html @@ -0,0 +1,33 @@ + +
+ +
+ + +
+ +
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.scss new file mode 100644 index 0000000000..66aec79aa4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.scss @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-segmented-button-widget { + width: 100%; + height: 100%; + position: relative; + + .tb-segmented-button-container { + height: 100%; + + .tb-widget-button-toggle { + display: block; + height: 100%; + } + } + + > div.tb-segmented-button-widget-title-panel { + position: absolute; + top: 12px; + left: 12px; + right: 12px; + z-index: 2; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.ts new file mode 100644 index 0000000000..6c7ee3eef8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.ts @@ -0,0 +1,118 @@ +/// +/// 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 { + ButtonToggleAppearance, + segmentedButtonDefaultSettings, + SegmentedButtonWidgetSettings +} from '@home/components/widget/lib/button/segmented-button-widget.models'; +import { ComponentStyle } from '@shared/models/widget-settings.models'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; + +@Component({ + selector: 'tb-segmented-button-widget', + templateUrl: './segmented-button-widget.component.html', + styleUrls: ['../action/action-widget.scss', './segmented-button-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class SegmentedButtonWidgetComponent extends + BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + settings: SegmentedButtonWidgetSettings; + + overlayStyle: ComponentStyle = {}; + + value = false; + disabled = false; + + autoScale: boolean; + appearance: ButtonToggleAppearance; + + constructor(protected imagePipe: ImagePipe, + protected sanitizer: DomSanitizer, + protected cd: ChangeDetectorRef) { + super(cd); + } + + ngOnInit(): void { + super.ngOnInit(); + this.settings = {...segmentedButtonDefaultSettings, ...this.ctx.settings}; + + this.autoScale = this.settings.appearance.autoScale; + + const getInitialStateSettings = + {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.initial-state')}; + this.createValueGetter(getInitialStateSettings, ValueType.BOOLEAN, { + next: (value) => this.onValue(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) + }); + + this.appearance = this.settings.appearance; + } + + ngAfterViewInit(): void { + super.ngAfterViewInit(); + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + public onInit() { + super.onInit(); + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + private onValue(value: boolean): void { + const newValue = !!value; + if (this.value !== newValue) { + this.value = newValue; + this.appearance = this.settings.appearance; + this.cd.markForCheck(); + } + } + + private onDisabled(value: boolean): void { + const newDisabled = !!value; + if (this.disabled !== newDisabled) { + this.disabled = newDisabled; + this.cd.markForCheck(); + } + } + + public onClick(_$event: MatButtonToggleChange) { + if (!this.ctx.isEdit && !this.ctx.isPreview) { + if (_$event.value) { + this.ctx.actionsApi.onWidgetAction(event, this.settings.leftButtonClick); + } else { + this.ctx.actionsApi.onWidgetAction(event, this.settings.rightButtonClick); + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts new file mode 100644 index 0000000000..611812f244 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts @@ -0,0 +1,346 @@ +/// +/// 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 { DataToValueType, GetValueAction, GetValueSettings } from '@shared/models/action-widget-settings.models'; +import { cssUnit, Font } from '@shared/models/widget-settings.models'; +import { defaultWidgetAction, WidgetAction } from '@shared/models/widget.models'; + +const defaultMainColor = '#305680'; +const defaultBackgroundColor = '#E8E8E8'; + +export const defaultMainColorDisabled = 'rgba(0, 0, 0, 0.38)'; +export const defaultBackgroundColorDisabled = 'rgba(0, 0, 0, 0.03)'; +export const defaultBorderColorDisabled = defaultBackgroundColorDisabled; + +const defaultBorderColor = defaultBackgroundColor; + +export enum SegmentedButtonLayout { + squared = 'squared', + rounded = 'rounded' +} + +export type SegmentedButtonAppearanceType = 'left' | 'right'; +export type SegmentedButtonColorStylesType = 'selected' | 'unselected'; + +export const segmentedButtonLayouts = Object.keys(SegmentedButtonLayout) as SegmentedButtonLayout[]; + +export const segmentedButtonLayoutTranslations = new Map( + [ + [SegmentedButtonLayout.squared, 'widgets.segmented-button.layout-squared'], + [SegmentedButtonLayout.rounded, 'widgets.segmented-button.layout-rounded'] + ] +); + +export const segmentedButtonLayoutImages = new Map( + [ + [SegmentedButtonLayout.squared, 'assets/widget/segmented-button/squared-layout.svg'], + [SegmentedButtonLayout.rounded, 'assets/widget/segmented-button/rounded-layout.svg'] + ] +); + +export const segmentedButtonLayoutBorder = new Map( + [ + [SegmentedButtonLayout.squared, '4px'], + [SegmentedButtonLayout.rounded, '40px'] + ] +); + +export interface SegmentedButtonAppearance { + showLabel: boolean; + label: string; + labelFont: Font; + showIcon: boolean; + icon: string; + iconSize: number; + iconSizeUnit: cssUnit; +} + +export interface SegmentedButtonStyles { + mainColor: string; + backgroundColor: string; + customStyle: WidgetButtonToggleCustomStyles; +} + +export interface ButtonToggleAppearance { + layout: SegmentedButtonLayout; + autoScale: boolean; + cardBorder: number; + cardBorderColor: string; + leftAppearance: SegmentedButtonAppearance; + rightAppearance: SegmentedButtonAppearance; + selectedStyle: SegmentedButtonStyles; + unselectedStyle: SegmentedButtonStyles; +} + +export interface SegmentedButtonWidgetSettings { + initialState: GetValueSettings; + disabledState: GetValueSettings; + leftButtonClick: WidgetAction; + rightButtonClick: WidgetAction; + + appearance: ButtonToggleAppearance; +} + +export const segmentedButtonDefaultAppearance: ButtonToggleAppearance = { + layout: SegmentedButtonLayout.squared, + autoScale: true, + cardBorder: 1, + cardBorderColor: '#305680', + leftAppearance: { + showLabel: true, + label: 'Traditional', + labelFont: { + family: 'Roboto', + weight: '500', + style: 'normal', + size: 14, + sizeUnit: 'px', + lineHeight: '18px' + }, + showIcon: true, + icon: 'home', + iconSize: 24, + iconSizeUnit: 'px', + }, + rightAppearance: { + showLabel: true, + label: 'Hi-Perf', + labelFont: { + family: 'Roboto', + weight: '500', + style: 'normal', + size: 14, + sizeUnit: 'px', + lineHeight: '18px' + }, + showIcon: true, + icon: 'home', + iconSize: 24, + iconSizeUnit: 'px', + }, + selectedStyle: { + mainColor: '#FFFFFF', + backgroundColor: '#00695C', + customStyle: { + enabled: null, + hovered: null, + disabled: null + } + }, + unselectedStyle: { + mainColor: '#000000C2', + backgroundColor: '#E8E8E8', + customStyle: { + enabled: null, + hovered: null, + disabled: null + } + } +} + +export const segmentedButtonDefaultSettings: SegmentedButtonWidgetSettings = { + initialState: { + action: GetValueAction.DO_NOTHING, + defaultValue: true, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + getAlarmStatus: { + severityList: null, + typeList: null + }, + 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' + }, + getAlarmStatus: { + severityList: null, + typeList: null + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + leftButtonClick: defaultWidgetAction(), + rightButtonClick: defaultWidgetAction(), + appearance: segmentedButtonDefaultAppearance +}; + + +const mainCheckedColorVarPrefix = '--tb-widget-button-toggle-main-checked-color-'; +const backgroundCheckedColorVarPrefix = '--tb-widget-button-toggle-background-checked-color-'; +const borderCheckedColorVarPrefix = '--tb-widget-button-toggle-border-checked-color-'; + +const mainUncheckedColorVarPrefix = '--tb-widget-button-toggle-main-unchecked-color-'; +const backgroundUncheckedColorVarPrefix = '--tb-widget-button-toggle-background-unchecked-color-'; +const borderUncheckedColorVarPrefix = '--tb-widget-button-toggle-border-unchecked-color-'; + +export enum WidgetButtonToggleState { + enabled = 'enabled', + hovered = 'hovered', + disabled = 'disabled' +} + +export const widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[]; + +export const widgetButtonToggleStatesTranslations = new Map( + [ + [WidgetButtonToggleState.enabled, 'widgets.button-state.enabled'], + [WidgetButtonToggleState.hovered, 'widgets.button-state.hovered'], + [WidgetButtonToggleState.disabled, 'widgets.button-state.disabled'] + ] +); + +export interface WidgetButtonToggleCustomStyle { + overrideMainColor?: boolean; + mainColor?: string; + overrideBackgroundColor?: boolean; + backgroundColor?: string; + overrideBorderColor?: boolean; + borderColor?: string; +} + +export type WidgetButtonToggleCustomStyles = Record; + +export abstract class ButtonToggleStateCssGenerator { + + constructor() {} + + public generateStateCss(selectedAppearance: SegmentedButtonStyles, unselectedAppearance: SegmentedButtonStyles): string { + const selectedColor = this.getColors(selectedAppearance); + const unselectedColor = this.getColors(unselectedAppearance); + + return `${mainCheckedColorVarPrefix}${this.state}: ${selectedColor.mainColor};\n`+ + `${backgroundCheckedColorVarPrefix}${this.state}: ${selectedColor.backgroundColor};\n`+ + `${borderCheckedColorVarPrefix}${this.state}: ${selectedColor.borderColor};\n`+ + `${mainUncheckedColorVarPrefix}${this.state}: ${unselectedColor.mainColor};\n`+ + `${backgroundUncheckedColorVarPrefix}${this.state}: ${unselectedColor.backgroundColor};\n`+ + `${borderUncheckedColorVarPrefix}${this.state}: ${unselectedColor.borderColor};`; + } + + private getColors(appearance: SegmentedButtonStyles) { + let mainColor = this.getMainColor(appearance); + let backgroundColor = this.getBackgroundColor(appearance); + let borderColor = this.getBorderColor(); + const stateCustomStyle = appearance.customStyle[this.state]; + if (stateCustomStyle?.overrideMainColor && stateCustomStyle?.mainColor) { + mainColor = stateCustomStyle.mainColor; + } + if (stateCustomStyle?.overrideBackgroundColor && stateCustomStyle?.backgroundColor) { + backgroundColor = stateCustomStyle.backgroundColor; + } + if (stateCustomStyle?.overrideBorderColor && stateCustomStyle?.borderColor) { + borderColor = stateCustomStyle.borderColor; + } + return { + mainColor, + backgroundColor, + borderColor + } + } + + protected abstract get state(): WidgetButtonToggleState; + + protected getMainColor(appearance: SegmentedButtonStyles): string { + return appearance.mainColor || defaultMainColor; + } + + protected getBackgroundColor(appearance: SegmentedButtonStyles): string { + return appearance.backgroundColor || defaultBackgroundColor; + } + + protected getBorderColor(): string { + return defaultBorderColor; + } +} + +class EnabledButtonStateCssGenerator extends ButtonToggleStateCssGenerator { + + protected get state(): WidgetButtonToggleState { + return WidgetButtonToggleState.enabled; + } +} + +class HoveredButtonStateCssGenerator extends ButtonToggleStateCssGenerator { + + protected get state(): WidgetButtonToggleState { + return WidgetButtonToggleState.hovered; + } +} + +class DisabledButtonStateCssGenerator extends ButtonToggleStateCssGenerator { + + protected get state(): WidgetButtonToggleState { + return WidgetButtonToggleState.disabled; + } + + protected getMainColor(): string { + return defaultMainColorDisabled; + } + + protected getBackgroundColor(): string { + return defaultBackgroundColorDisabled; + } + + protected getBorderColor(): string { + return defaultBorderColorDisabled; + } +} + +const buttonToggleStateCssGeneratorsMap = new Map( + [ + [WidgetButtonToggleState.enabled, new EnabledButtonStateCssGenerator()], + [WidgetButtonToggleState.hovered, new HoveredButtonStateCssGenerator()], + [WidgetButtonToggleState.disabled, new DisabledButtonStateCssGenerator()] + ] +); + +const widgetButtonCssSelector = '.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header'; + +export const generateWidgetButtonToggleAppearanceCss = (selectedAppearance: SegmentedButtonStyles, unselectedAppearance: SegmentedButtonStyles): string => { + let statesCss = ''; + for (const state of widgetButtonToggleStates) { + const generator = buttonToggleStateCssGeneratorsMap.get(state); + statesCss += `\n${generator.generateStateCss(selectedAppearance, unselectedAppearance)}`; + } + return `${widgetButtonCssSelector} {\n`+ + `${statesCss}\n`+ + `}`; +}; + +export const generateWidgetButtonToggleBorderLayout = (layout: SegmentedButtonLayout): string => { + return `${widgetButtonCssSelector} {\n`+ + `--tb-widget-button-toggle-border-radius: ${segmentedButtonLayoutBorder.get(layout)}\n`+ + `}`; +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html new file mode 100644 index 0000000000..4cf82918cb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html @@ -0,0 +1,263 @@ + + +
+
widgets.action-button.behavior
+
+
widgets.button-state.initial-state
+ +
+ +
+
widgets.action-button.left-button-click
+ + +
+
+
widgets.action-button.right-button-click
+ + +
+ +
+
widgets.button-state.disabled-state
+ +
+
+
+
widget-config.appearance
+ + + + {{ segmentedButtonLayoutTranslationMap.get(layout) | translate }} + + +
+ + {{ 'widgets.button.auto-scale' | translate }} + +
+
+
widgets.segmented-button.card-border
+
+ + +
px
+
+ + +
+
+
+ +
+
+
widgets.segmented-button.button-appearance
+ + {{ 'widgets.segmented-button.left' | translate }} + {{ 'widgets.segmented-button.right' | translate }} + +
+
+
+ + {{ 'widgets.button.label' | translate }} + +
+ + + + + +
+
+
+ + {{ 'widgets.button.icon' | translate }} + +
+ + + + + + +
+
+
+ +
+
+ + {{ 'widgets.button.label' | translate }} + +
+ + + + + +
+
+
+ + {{ 'widgets.button.icon' | translate }} + +
+ + + + + + +
+
+
+
+ +
+
+
widgets.segmented-button.color-styles
+ + {{ 'widgets.segmented-button.selected' | translate }} + {{ 'widgets.segmented-button.unselected' | translate }} + +
+
+
+
{{ 'widgets.button.color-palette' | translate }}
+
+
+
widgets.button.main
+ + +
+ +
+
widgets.button.background
+ + +
+
+
+
+ + + +
widgets.button.custom-styles
+
+
+ +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+
+
+
+
+
+
+
{{ 'widgets.button.color-palette' | translate }}
+
+
+
widgets.button.main
+ + +
+ +
+
widgets.button.background
+ + +
+
+
+
+ + + +
widgets.button.custom-styles
+
+
+ +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+ + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts new file mode 100644 index 0000000000..979cf2c078 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts @@ -0,0 +1,134 @@ +/// +/// 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 { + SegmentedButtonAppearanceType, + SegmentedButtonColorStylesType, + segmentedButtonDefaultSettings, + segmentedButtonLayoutBorder, + segmentedButtonLayoutImages, + segmentedButtonLayouts, + segmentedButtonLayoutTranslations, + WidgetButtonToggleState, + widgetButtonToggleStatesTranslations +} from '@home/components/widget/lib/button/segmented-button-widget.models'; + +@Component({ + selector: 'tb-segmented-button-widget-settings', + templateUrl: './segmented-button-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss'] +}) +export class SegmentedButtonWidgetSettingsComponent 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.segmentedButtonLayoutBorderMap.get(this.segmentedButtonWidgetSettingsForm.get('appearance.layout').value); + } + + segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'left'; + segmentedButtonColorStylesType: SegmentedButtonColorStylesType = 'selected'; + + widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[]; + widgetButtonToggleStatesTranslationsMap = widgetButtonToggleStatesTranslations; + + segmentedButtonLayouts = segmentedButtonLayouts; + segmentedButtonLayoutTranslationMap = segmentedButtonLayoutTranslations; + segmentedButtonLayoutImageMap = segmentedButtonLayoutImages; + segmentedButtonLayoutBorderMap = segmentedButtonLayoutBorder; + + valueType = ValueType; + + segmentedButtonWidgetSettingsForm: UntypedFormGroup; + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.segmentedButtonWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...segmentedButtonDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.segmentedButtonWidgetSettingsForm = this.fb.group({ + initialState: [settings.initialState, []], + leftButtonClick: [settings.leftButtonClick, []], + rightButtonClick: [settings.rightButtonClick, []], + disabledState: [settings.disabledState, []], + + appearance: this.fb.group({ + layout: [settings.appearance.layout, []], + autoScale: [settings.appearance.autoScale, []], + cardBorder: [settings.appearance.cardBorder, []], + cardBorderColor: [settings.appearance.cardBorderColor, []], + leftAppearance: this.fb.group({ + showLabel: [settings.appearance.leftAppearance.showLabel, []], + label: [settings.appearance.leftAppearance.label, []], + labelFont: [settings.appearance.leftAppearance.labelFont, []], + showIcon: [settings.appearance.leftAppearance.showIcon, []], + icon: [settings.appearance.leftAppearance.icon, []], + iconSize: [settings.appearance.leftAppearance.iconSize, []], + iconSizeUnit: [settings.appearance.leftAppearance.iconSizeUnit, []], + }), + rightAppearance: this.fb.group({ + showLabel: [settings.appearance.rightAppearance.showLabel, []], + label: [settings.appearance.rightAppearance.label, []], + labelFont: [settings.appearance.rightAppearance.labelFont, []], + showIcon: [settings.appearance.rightAppearance.showIcon, []], + icon: [settings.appearance.rightAppearance.icon, []], + iconSize: [settings.appearance.rightAppearance.iconSize, []], + iconSizeUnit: [settings.appearance.rightAppearance.iconSizeUnit, []], + }), + selectedStyle: this.fb.group({ + mainColor: [settings.appearance.selectedStyle.mainColor, []], + backgroundColor: [settings.appearance.selectedStyle.backgroundColor, []], + customStyle: this.fb.group({ + enabled: [settings.appearance.selectedStyle.customStyle.enabled, []], + hovered: [settings.appearance.selectedStyle.customStyle.hovered, []], + disabled: [settings.appearance.selectedStyle.customStyle.disabled, []], + }) + }), + unselectedStyle: this.fb.group({ + mainColor: [settings.appearance.unselectedStyle.mainColor, []], + backgroundColor: [settings.appearance.unselectedStyle.backgroundColor, []], + customStyle: this.fb.group({ + enabled: [settings.appearance.unselectedStyle.customStyle.enabled, []], + hovered: [settings.appearance.unselectedStyle.customStyle.hovered, []], + disabled: [settings.appearance.unselectedStyle.customStyle.disabled, []], + }) + }), + }) + }); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.html new file mode 100644 index 0000000000..f0e50b03e6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.html @@ -0,0 +1,92 @@ + +
+
{{ widgetButtonToggleStatesTranslationsMap.get(state) | translate }}
+
+
+ + {{ 'widgets.button.main' | translate }} + + + +
+
+ + {{ 'widgets.button.background' | translate }} + + + +
+
+ + {{ 'widgets.button.border' | translate }} + + + +
+
+
+ widgets.button.preview +
+ + +
+
+
+ + + +
+ +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.scss new file mode 100644 index 0000000000..dfbbb91620 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-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; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts new file mode 100644 index 0000000000..98ced2dc2e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component.ts @@ -0,0 +1,210 @@ +/// +/// 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, + ViewChild, + 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 +} from '@shared/components/button/widget-button.models'; +import { merge } from 'rxjs'; +import { deepClone } from '@core/utils'; +import { WidgetButtonToggleComponent } from '@shared/components/button/widget-button-toggle.component'; +import { + ButtonToggleAppearance, + SegmentedButtonStyles, + WidgetButtonToggleCustomStyle, + WidgetButtonToggleState, + widgetButtonToggleStates, + widgetButtonToggleStatesTranslations +} from '@home/components/widget/lib/button/segmented-button-widget.models'; + +@Component({ + selector: 'tb-widget-button-toggle-custom-style-panel', + templateUrl: './widget-button-toggle-custom-style-panel.component.html', + providers: [], + styleUrls: ['./widget-button-toggle-custom-style-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class WidgetButtonToggleCustomStylePanelComponent extends PageComponent implements OnInit { + + @ViewChild('widgetButtonTogglePreview') + widgetButtonTogglePreview: WidgetButtonToggleComponent; + + @Input() + appearance: ButtonToggleAppearance; + + @Input() + value: boolean = false; + + @Input() + borderRadius: string; + + @Input() + autoScale: boolean; + + @Input() + state: WidgetButtonToggleState; + + @Input() + customStyle: WidgetButtonToggleCustomStyle; + + private popoverValue: TbPopoverComponent; + + @Input() + set popover(popover: TbPopoverComponent) { + this.popoverValue = popover; + popover.tbAnimationDone.subscribe(() => { + this.widgetButtonTogglePreview?.validateSize(); + }); + } + + get popover(): TbPopoverComponent { + return this.popoverValue; + } + + @Output() + customStyleApplied = new EventEmitter(); + + widgetButtonToggleStatesTranslationsMap = widgetButtonToggleStatesTranslations; + + WidgetButtonToggleState = WidgetButtonToggleState; + + previewAppearance: ButtonToggleAppearance; + + copyFromStates: WidgetButtonToggleState[]; + + customStyleFormGroup: UntypedFormGroup; + + style: SegmentedButtonStyles; + + constructor(private fb: UntypedFormBuilder, + protected store: Store, + private cd: ChangeDetectorRef) { + super(store); + } + + ngOnInit(): void { + this.style = this.value ? this.appearance.selectedStyle : this.appearance.unselectedStyle; + this.copyFromStates = widgetButtonToggleStates.filter(state => + state !== this.state && !!this.style.customStyle[state]); + this.customStyleFormGroup = this.fb.group( + { + overrideMainColor: [false, []], + mainColor: [null, []], + overrideBackgroundColor: [false, []], + backgroundColor: [null, []], + overrideBorderColor: [false, []], + borderColor: [null, []] + } + ); + merge(this.customStyleFormGroup.get('overrideMainColor').valueChanges, + this.customStyleFormGroup.get('overrideBackgroundColor').valueChanges, + this.customStyleFormGroup.get('overrideBorderColor').valueChanges) + .subscribe(() => { + this.updateValidators(); + }); + this.customStyleFormGroup.valueChanges.subscribe(() => { + this.updatePreviewAppearance(); + }); + this.setStyle(this.customStyle); + } + + copyStyle(state: WidgetButtonToggleState) { + this.customStyle = deepClone(this.style[state]); + this.setStyle(this.customStyle); + this.customStyleFormGroup.markAsDirty(); + } + + cancel() { + this.popover?.hide(); + } + + applyCustomStyle() { + const customStyle: SegmentedButtonStyles = this.customStyleFormGroup.value; + this.customStyleApplied.emit(customStyle); + } + + private setStyle(customStyle?: WidgetButtonToggleCustomStyle): void { + let mainColor = this.state === WidgetButtonToggleState.disabled ? defaultMainColorDisabled : this.style.mainColor; + if (customStyle?.overrideMainColor) { + mainColor = customStyle?.mainColor; + } + let backgroundColor = this.state === WidgetButtonToggleState.disabled ? defaultBackgroundColorDisabled : this.style.backgroundColor; + if (customStyle?.overrideBackgroundColor) { + backgroundColor = customStyle?.backgroundColor; + } + let borderColor = this.state === WidgetButtonToggleState.disabled ? defaultBackgroundColorDisabled : this.style.backgroundColor; + if (customStyle?.overrideBorderColor) { + borderColor = customStyle?.borderColor; + } + this.customStyleFormGroup.patchValue({ + overrideMainColor: customStyle?.overrideMainColor, + mainColor, + overrideBackgroundColor: customStyle?.overrideBackgroundColor, + backgroundColor, + overrideBorderColor: customStyle?.overrideBorderColor, + borderColor + }, {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 overrideBorderColor: boolean = this.customStyleFormGroup.get('overrideBorderColor').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 (overrideBorderColor) { + this.customStyleFormGroup.get('borderColor').enable({emitEvent: false}); + } else { + this.customStyleFormGroup.get('borderColor').disable({emitEvent: false}); + } + } + + private updatePreviewAppearance() { + this.previewAppearance = deepClone(this.appearance); + if (this.value) { + this.previewAppearance.selectedStyle.customStyle[this.state] = this.customStyleFormGroup.value; + } else { + this.previewAppearance.unselectedStyle.customStyle[this.state] = this.customStyleFormGroup.value; + } + this.cd.markForCheck(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html new file mode 100644 index 0000000000..624d17af16 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.html @@ -0,0 +1,44 @@ + +
+
+ + + +
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.scss new file mode 100644 index 0000000000..6badf1f804 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-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-toggle { + width: 84px; + height: 100%; + } + @media #{$mat-gt-xs} { + width: 188px; + tb-widget-button-toggle { + width: 124px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts new file mode 100644 index 0000000000..366c78a0c6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component.ts @@ -0,0 +1,169 @@ +/// +/// 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 { TbPopoverService } from '@shared/components/popover.service'; +import { MatIconButton } from '@angular/material/button'; +import { deepClone } from '@core/utils'; +import { + ButtonToggleAppearance, + WidgetButtonToggleCustomStyle, + WidgetButtonToggleState +} from '@home/components/widget/lib/button/segmented-button-widget.models'; +import { + WidgetButtonToggleCustomStylePanelComponent +} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component'; + +@Component({ + selector: 'tb-widget-button-toggle-custom-style', + templateUrl: './widget-button-toggle-custom-style.component.html', + styleUrls: ['./widget-button-toggle-custom-style.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => WidgetButtonToggleCustomStyleComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class WidgetButtonToggleCustomStyleComponent implements OnInit, OnChanges, ControlValueAccessor { + + @Input() + disabled = false; + + @Input() + value = false; + + @Input() + appearance: ButtonToggleAppearance; + + @Input() + borderRadius: string; + + @Input() + autoScale: boolean; + + @Input() + state: WidgetButtonToggleState; + + widgetButtonToggleState = WidgetButtonToggleState; + + modelValue: WidgetButtonToggleCustomStyle; + + previewAppearance: ButtonToggleAppearance; + + 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: WidgetButtonToggleCustomStyle): 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, + autoScale: this.autoScale, + state: this.state, + value: this.value, + customStyle: this.modelValue + }; + const widgetButtonCustomStylePanelPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, WidgetButtonToggleCustomStylePanelComponent, + ['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: WidgetButtonToggleCustomStyle): void { + this.modelValue = value; + this.updatePreviewAppearance(); + this.propagateChange(this.modelValue); + } + + private updatePreviewAppearance() { + this.previewAppearance = deepClone(this.appearance); + if (this.modelValue) { + if (this.value) { + this.previewAppearance.selectedStyle.customStyle[this.state] = this.modelValue; + } else { + this.previewAppearance.unselectedStyle.customStyle[this.state] = this.modelValue; + } + } + this.cd.markForCheck(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index fbfffc973f..037096f406 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -158,6 +158,12 @@ import { import { ScadaSymbolObjectSettingsComponent } from '@home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component'; +import { + WidgetButtonToggleCustomStyleComponent +} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style.component'; +import { + WidgetButtonToggleCustomStylePanelComponent +} from '@home/components/widget/lib/settings/common/button/widget-button-toggle-custom-style-panel.component'; @NgModule({ declarations: [ @@ -196,7 +202,9 @@ import { WidgetActionSettingsPanelComponent, WidgetButtonAppearanceComponent, WidgetButtonCustomStyleComponent, + WidgetButtonToggleCustomStyleComponent, WidgetButtonCustomStylePanelComponent, + WidgetButtonToggleCustomStylePanelComponent, TimeSeriesChartAxisSettingsComponent, TimeSeriesChartThresholdsPanelComponent, TimeSeriesChartThresholdRowComponent, @@ -261,7 +269,9 @@ import { WidgetActionSettingsPanelComponent, WidgetButtonAppearanceComponent, WidgetButtonCustomStyleComponent, + WidgetButtonToggleCustomStyleComponent, WidgetButtonCustomStylePanelComponent, + WidgetButtonToggleCustomStylePanelComponent, TimeSeriesChartAxisSettingsComponent, TimeSeriesChartThresholdsPanelComponent, TimeSeriesChartThresholdRowComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 37f61b11fb..4b5f3a1ad0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -368,6 +368,9 @@ import { import { ScadaSymbolWidgetSettingsComponent } from '@home/components/widget/lib/settings/scada/scada-symbol-widget-settings.component'; +import { + SegmentedButtonWidgetSettingsComponent +} from '@home/components/widget/lib/settings/button/segmented-button-widget-settings.component'; @NgModule({ declarations: [ @@ -483,6 +486,7 @@ ScadaSymbolWidgetSettingsComponent BarChartWithLabelsWidgetSettingsComponent, SingleSwitchWidgetSettingsComponent, ActionButtonWidgetSettingsComponent, + SegmentedButtonWidgetSettingsComponent, CommandButtonWidgetSettingsComponent, PowerButtonWidgetSettingsComponent, SliderWidgetSettingsComponent, @@ -619,6 +623,7 @@ ScadaSymbolWidgetSettingsComponent BarChartWithLabelsWidgetSettingsComponent, SingleSwitchWidgetSettingsComponent, ActionButtonWidgetSettingsComponent, + SegmentedButtonWidgetSettingsComponent, CommandButtonWidgetSettingsComponent, PowerButtonWidgetSettingsComponent, SliderWidgetSettingsComponent, @@ -723,6 +728,7 @@ export const widgetSettingsComponentsMap: {[key: string]: Type +
+ + +
+ {{ appearance.leftAppearance.icon }} + {{ leftLabel$ | async }} +
+
+ +
+ {{ appearance.rightAppearance.icon }} + {{ rightLabel$ | async }} +
+
+
+
diff --git a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.scss b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.scss new file mode 100644 index 0000000000..4ce8c69ddb --- /dev/null +++ b/ui-ngx/src/app/shared/components/button/widget-button-toggle.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. + */ + +$defaultBorderRadius: 4px; + +$defaultCheckedMainColor: #FFFFFF; +$defaultCheckedBackgroundColor: #305680; +$defaultCheckedBorderColor: #305680; + +$defaultUncheckedMainColor: rgba(0, 0, 0, 0.76); +$defaultUncheckedBackgroundColor: #E8E8E8; +$defaultUncheckedBorderColor: #E8E8E8; + +$defaultCheckedMainColorDisabled: #FFFFFFCC; +$defaultCheckedBackgroundColorDisabled: rgba(0, 0, 0, 0.12); +$defaultCheckedBorderColorDisabled: rgba(0, 0, 0, 0.12); + +$defaultUncheckedMainColorDisabled: rgba(0, 0, 0, 0.12); +$defaultUncheckedBackgroundColorDisabled: #E8E8E8; +$defaultUncheckedBorderColorDisabled: #E8E8E8; + +$buttonBorderRadius: var(--tb-widget-button-toggle-border-radius, $defaultBorderRadius); + +$mainCheckedColorEnabled: var(--tb-widget-button-toggle-main-checked-color-enabled, $defaultCheckedMainColor); +$backgroundCheckedColorEnabled: var(--tb-widget-button-toggle-background-checked-color-enabled, $defaultCheckedBackgroundColor); +$borderCheckedColorEnabled: var(--tb-widget-button-toggle-border-checked-color-enabled, $defaultCheckedBorderColor); + +$mainUncheckedColorEnabled: var(--tb-widget-button-toggle-main-unchecked-color-enabled, $defaultUncheckedMainColor); +$backgroundUncheckedColorEnabled: var(--tb-widget-button-toggle-background-unchecked-color-enabled, $defaultUncheckedBackgroundColor); +$borderUncheckedColorEnabled: var(--tb-widget-button-toggle-border-unchecked-color-enabled, $defaultUncheckedBorderColor); + +$mainCheckedColorDisabled: var(--tb-widget-button-toggle-main-checked-color-disabled, $defaultCheckedMainColorDisabled); +$backgroundCheckedColorDisabled: var(--tb-widget-button-toggle-background-checked-color-disabled, $defaultCheckedBackgroundColorDisabled); +$borderCheckedColorDisabled: var(--tb-widget-button-toggle-border-checked-color-disabled, $defaultCheckedBorderColorDisabled); + +$mainUncheckedColorDisabled: var(--tb-widget-button-toggle-main-unchecked-color-disabled, $defaultUncheckedMainColorDisabled); +$backgroundUncheckedColorDisabled: var(--tb-widget-button-toggle-background-unchecked-color-disabled, $defaultUncheckedBackgroundColorDisabled); +$borderUncheckedColorDisabled: var(--tb-widget-button-toggle-border-unchecked-color-disabled, $defaultUncheckedBorderColorDisabled); + +$mainCheckedColorHovered: var(--tb-widget-button-toggle-main-checked-color-hovered, $defaultCheckedMainColor); +$backgroundCheckedColorHovered: var(--tb-widget-button-toggle-background-checked-color-hovered, $defaultCheckedBackgroundColor); +$borderCheckedColorHovered: var(--tb-widget-button-toggle-border-checked-color-hovered, $defaultCheckedBorderColor); + +$mainUncheckedColorHovered: var(--tb-widget-button-toggle-main-unchecked-color-hovered, $defaultCheckedMainColor); +$backgroundUncheckedColorHovered: var(--tb-widget-button-toggle-background-unchecked-color-hovered, $defaultCheckedBackgroundColor); +$borderUncheckedColorHovered: var(--tb-widget-button-toggle-border-unchecked-color-hovered, $defaultCheckedBorderColor); + +@mixin _tb-widget-button-styles($main, $background, $boxShadow) { + color: $main; + background-color: $background; + border: 1px solid $boxShadow; +} + +:host { + max-width: 100%; + .tb-toggle-container { + display: block; + height: 100%; + } + .tb-toggle-header { + transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1); + } + + .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; + } + } +} + +:host ::ng-deep { + .mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header { + overflow: visible; + width: 100%; + height: 100%; + padding: 3px; + border: 1px solid; + background: transparent; + .mat-button-toggle + .mat-button-toggle { + border-left: none; + } + .mat-button-toggle.mat-button-toggle-appearance-standard { + flex: 1; + width: 50%; + border-radius: $buttonBorderRadius; + + &.tb-hover-state { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainUncheckedColorHovered, $backgroundUncheckedColorHovered, $borderUncheckedColorHovered); + } + &.mat-button-toggle-checked { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainCheckedColorHovered, $backgroundCheckedColorHovered, $borderCheckedColorHovered); + } + } + } + + .mat-button-toggle-button { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + border-radius: $buttonBorderRadius; + @include _tb-widget-button-styles($mainUncheckedColorEnabled, $backgroundUncheckedColorEnabled, $borderUncheckedColorEnabled); + .mat-button-toggle-label-content { + .mat-pseudo-checkbox { + display: none; + } + } + } + &.mat-button-toggle-checked { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainCheckedColorEnabled, $backgroundCheckedColorEnabled, $borderCheckedColorEnabled); + + } + } + + } + &.tb-pointer-events { + .mat-button-toggle.mat-button-toggle-appearance-standard { + .mat-button-toggle-button { + pointer-events: none; + } + .mat-button-toggle-focus-overlay, .mat-button-toggle-ripple { + opacity: 0; + } + } + } + &:not(:disabled):not(.tb-disabled-state):not(.tb-pointer-events) { + .mat-button-toggle.mat-button-toggle-appearance-standard { + &:hover { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainUncheckedColorHovered, $backgroundUncheckedColorHovered, $borderUncheckedColorHovered); + } + &.mat-button-toggle-checked { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainCheckedColorHovered, $backgroundCheckedColorHovered, $borderCheckedColorHovered); + } + } + } + } + } + &:disabled, &.tb-disabled-state { + pointer-events: none; + .mat-button-toggle.mat-button-toggle-appearance-standard { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainUncheckedColorDisabled, $backgroundUncheckedColorDisabled, $borderUncheckedColorDisabled); + } + + &.mat-button-toggle-checked { + .mat-button-toggle-button { + @include _tb-widget-button-styles($mainCheckedColorDisabled, $backgroundCheckedColorDisabled, $borderCheckedColorDisabled); + } + } + } + .mat-button-toggle-focus-overlay, .mat-button-toggle-ripple { + opacity: 0; + } + } + .mat-button-toggle-focus-overlay, .mat-button-toggle-ripple { + border-radius: inherit; + } + } +} diff --git a/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts new file mode 100644 index 0000000000..4f281b2aa0 --- /dev/null +++ b/ui-ngx/src/app/shared/components/button/widget-button-toggle.component.ts @@ -0,0 +1,234 @@ +/// +/// 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 +} from '@angular/core'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { ComponentStyle, iconStyle, textStyle, validateCssSize } from '@shared/models/widget-settings.models'; +import { UtilsService } from '@core/services/utils.service'; +import { Observable, of } from 'rxjs'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { isDefinedAndNotNull } from '@core/utils'; +import { + generateWidgetButtonToggleAppearanceCss, + generateWidgetButtonToggleBorderLayout, + segmentedButtonDefaultAppearance, + segmentedButtonLayoutBorder +} from '@home/components/widget/lib/button/segmented-button-widget.models'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; + +const initialButtonHeight = 60; +const horizontalLayoutPadding = 10; + +@Component({ + selector: 'tb-widget-button-toggle', + templateUrl: './widget-button-toggle.component.html', + styleUrls: ['./widget-button-toggle.component.scss'] +}) +export class WidgetButtonToggleComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { + + @ViewChild('toggleGroupContainer', {static: false}) + toggleGroupContainer: ElementRef; + + @ViewChild('widgetButton', {read: ElementRef}) + widgetButton: ElementRef; + + @ViewChild('leftButtonContent', {static: false}) + leftButtonContent: ElementRef; + @ViewChild('rightButtonContent', {static: false}) + rightButtonContent: ElementRef; + + @Input() + appearance = segmentedButtonDefaultAppearance; + + @Input() + borderRadius: string; + + @Input() + autoScale: boolean; + + @Input() + @coerceBoolean() + value = false; + + @Input() + @coerceBoolean() + disabled = false; + + @Input() + @coerceBoolean() + hovered = false; + + @Input() + @coerceBoolean() + disableEvents = false; + + @Input() + ctx: WidgetContext; + + @Output() + clicked = new EventEmitter(); + + leftLabel$: Observable; + rightLabel$: Observable; + + leftIconStyle: ComponentStyle = {}; + rightIconStyle: ComponentStyle = {}; + + leftLabelStyle: ComponentStyle = {}; + rightLabelStyle: ComponentStyle = {}; + + computedBorderColor: string; + computedBorderWidth: string; + computedBorderRadius: string; + + 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(); + } else if (propName === 'borderRadius') { + this.updateBorderRadius(); + } else if (propName === 'autoScale') { + this.updateAutoScale(); + } + } + } + } + + ngAfterViewInit(): void { + this.updateAutoScale(); + } + + ngOnDestroy(): void { + if (this.buttonResize$) { + this.buttonResize$.disconnect(); + } + this.clearAppearanceCss(); + } + + public validateSize() { + if (this.appearance.autoScale && this.widgetButton.nativeElement) { + this.onResize(); + } + } + + private updateAppearance(): void { + this.clearAppearanceCss(); + this.computedBorderColor = this.appearance.cardBorderColor; + this.computedBorderWidth = validateCssSize(this.appearance.cardBorder.toString()); + if (this.appearance.leftAppearance.showIcon) { + this.leftIconStyle = iconStyle(this.appearance.leftAppearance.iconSize, this.appearance.leftAppearance.iconSizeUnit); + } + if (this.appearance.rightAppearance.showIcon) { + this.rightIconStyle = iconStyle(this.appearance.rightAppearance.iconSize, this.appearance.rightAppearance.iconSizeUnit); + } + if (this.appearance.leftAppearance.showLabel) { + this.leftLabelStyle = textStyle(this.appearance.leftAppearance.labelFont); + this.leftLabel$ = this.ctx ? this.ctx.registerLabelPattern(this.appearance.leftAppearance.label, this.leftLabel$) : of(this.appearance.leftAppearance.label); + } + if (this.appearance.rightAppearance.showLabel) { + this.rightLabelStyle = textStyle(this.appearance.rightAppearance.labelFont); + this.rightLabel$ = this.ctx ? this.ctx.registerLabelPattern(this.appearance.rightAppearance.label, this.rightLabel$) : of(this.appearance.rightAppearance.label); + } + this.updateBorderRadius(); + const appearanceCss = generateWidgetButtonToggleAppearanceCss(this.appearance.selectedStyle, this.appearance.unselectedStyle); + const layoutCss = generateWidgetButtonToggleBorderLayout(this.appearance.layout); + this.appearanceCssClass = this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement, + 'tb-widget-button', appearanceCss + layoutCss); + this.updateAutoScale(); + } + + private updateBorderRadius(): void { + if (this.borderRadius?.length) { + const validatedBorderRadius = validateCssSize(this.borderRadius); + if (validatedBorderRadius) { + this.computedBorderRadius = validatedBorderRadius; + } else { + this.computedBorderRadius = this.borderRadius; + } + } else { + this.computedBorderRadius = segmentedButtonLayoutBorder.get(this.appearance.layout); + } + + } + + 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.rightButtonContent && this.leftButtonContent) { + const autoScale = isDefinedAndNotNull(this.autoScale) ? this.autoScale : this.appearance.autoScale; + if (autoScale) { + this.buttonResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.buttonResize$.observe(this.widgetButton.nativeElement); + this.onResize(); + } else { + this.renderer.setStyle(this.widgetButton.nativeElement, 'transform', 'none'); + this.renderer.setStyle(this.widgetButton.nativeElement, 'width', '100%'); + } + } + } + + private onResize() { + const height = this.widgetButton.nativeElement.getBoundingClientRect().height; + const buttonScale = height / initialButtonHeight; + const buttonWidth = this.widgetButton.nativeElement.getBoundingClientRect().width; + const buttonHeight = this.widgetButton.nativeElement.getBoundingClientRect().height; + this.renderer.setStyle(this.leftButtonContent.nativeElement, 'transform', `scale(1)`); + this.renderer.setStyle(this.rightButtonContent.nativeElement, 'transform', `scale(1)`); + const contentWidth = this.leftButtonContent.nativeElement.getBoundingClientRect().width; + const contentHeight = this.leftButtonContent.nativeElement.getBoundingClientRect().height; + const maxScale = Math.max(1, buttonScale); + const scale = Math.min(Math.min((buttonWidth / 2 - horizontalLayoutPadding) / contentWidth, buttonHeight / contentHeight), maxScale); + this.renderer.setStyle(this.leftButtonContent.nativeElement, 'transform', `scale(${scale})`); + this.renderer.setStyle(this.rightButtonContent.nativeElement, 'transform', `scale(${scale})`); + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 02b5817800..07eb13f25d 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -218,6 +218,7 @@ import { CountryAutocompleteComponent } from '@shared/components/country-autocom import { CountryData } from '@shared/models/country.models'; import { SvgXmlComponent } from '@shared/components/svg-xml.component'; import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component'; +import { WidgetButtonToggleComponent } from '@shared/components/button/widget-button-toggle.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -419,6 +420,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) EmbedImageDialogComponent, ImageGalleryDialogComponent, WidgetButtonComponent, + WidgetButtonToggleComponent, HexInputComponent, ScadaSymbolInputComponent ], @@ -676,6 +678,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) EmbedImageDialogComponent, ImageGalleryDialogComponent, WidgetButtonComponent, + WidgetButtonToggleComponent, ScadaSymbolInputComponent ] }) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 405f4ae9d3..4619b14112 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5621,7 +5621,12 @@ "action-button": { "behavior": "Behavior", "on-click": "On click", - "on-click-hint": "Action triggered when the button is clicked" + "on-click-hint": "Action triggered when the button is clicked", + "left-button-click": "Left button click", + "left-button-click-hint": "Action while pressing on left button.", + "right-button-click": "Right button click", + "right-button-click-hint": "Action while pressing on right button.", + "button-click-hint": "Action while pressing on widget." }, "command-button": { "behavior": "Behavior", @@ -5666,6 +5671,18 @@ "vertical-fill": "Vertical fill", "button-appearance": "Button appearance" }, + "segmented-button": { + "layout": "Layout", + "layout-squared": "Squared", + "layout-rounded": "Rounded", + "card-border": "Card border", + "button-appearance": "Button appearance", + "left": "Left", + "right": "Right", + "color-styles": "Color styles", + "selected": "Selected", + "unselected": "Unselected" + }, "button": { "layout": "Layout", "outlined": "Outlined", @@ -5679,6 +5696,7 @@ "color-palette": "Color palette", "main": "Main", "background": "Background", + "border": "Border", "custom-styles": "Custom styles", "clear-style": "Clear style", "shadow": "Shadow", @@ -5692,11 +5710,16 @@ "activated-state-hint": "Configure condition under which the button is active.", "disabled-state": "Disabled state", "disabled-state-hint": "Configure condition under which the button is disabled.", + "initial-state": "Initial button", + "initial-state-hint": "Action to get the initial state of the component", "enabled": "Enabled", "hovered": "Hovered", "pressed": "Pressed", "activated": "Activated", - "disabled": "Disabled" + "disabled": "Disabled", + "initial": "Left button", + "left": "Left", + "right": "Right" }, "background": { "background": "Background", diff --git a/ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg b/ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg new file mode 100644 index 0000000000..2a0945030b --- /dev/null +++ b/ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-ngx/src/assets/widget/segmented-button/squared-layout.svg b/ui-ngx/src/assets/widget/segmented-button/squared-layout.svg new file mode 100644 index 0000000000..2c1d283e9a --- /dev/null +++ b/ui-ngx/src/assets/widget/segmented-button/squared-layout.svg @@ -0,0 +1 @@ + \ No newline at end of file From a9fe888223da9e9ddef920e89ecf6b07329780f8 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 28 Nov 2024 19:01:48 +0200 Subject: [PATCH 03/10] UI: Update layout image --- ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg | 2 +- ui-ngx/src/assets/widget/segmented-button/squared-layout.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg b/ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg index 2a0945030b..6f0963016c 100644 --- a/ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg +++ b/ui-ngx/src/assets/widget/segmented-button/rounded-layout.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui-ngx/src/assets/widget/segmented-button/squared-layout.svg b/ui-ngx/src/assets/widget/segmented-button/squared-layout.svg index 2c1d283e9a..6fb5abd30b 100644 --- a/ui-ngx/src/assets/widget/segmented-button/squared-layout.svg +++ b/ui-ngx/src/assets/widget/segmented-button/squared-layout.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 244bb8cd1ed7c5546a4eca5a1356ef123241f044 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 28 Nov 2024 19:36:49 +0200 Subject: [PATCH 04/10] UI: Refactoring --- .../main/data/json/system/widget_types/segmented_button.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/data/json/system/widget_types/segmented_button.json b/application/src/main/data/json/system/widget_types/segmented_button.json index 55e1165b83..967ed82315 100644 --- a/application/src/main/data/json/system/widget_types/segmented_button.json +++ b/application/src/main/data/json/system/widget_types/segmented_button.json @@ -22,7 +22,7 @@ "resources": [ { "link": "/api/images/system/segmented-button.svg", - "title": "\"Action button\" system widget image", + "title": "\"Segmented button\" system widget image", "type": "IMAGE", "subType": "IMAGE", "fileName": "segmented-button.svg", From c42cf2817e3b3728f5dca48933a41ffd9787518f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 2 Dec 2024 17:51:19 +0200 Subject: [PATCH 05/10] Housekeeper: interrupt task processing on timeout --- .../housekeeper/HousekeeperService.java | 6 +- .../processor/HousekeeperTaskProcessor.java | 11 ++++ .../LatestTsDeletionTaskProcessor.java | 2 +- .../TsHistoryDeletionTaskProcessor.java | 2 +- .../housekeeper/HousekeeperServiceTest.java | 58 ++++++++++++++++++- .../engine/util/SemaphoreWithTbMsgQueue.java | 2 +- 6 files changed, 74 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index 933f6fad3e..c69bc62692 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -117,9 +117,10 @@ public class HousekeeperService { throw new IllegalArgumentException("Unsupported task type " + taskType); } + Future future = null; try { long startTs = System.currentTimeMillis(); - Future future = taskExecutor.submit(() -> { + future = taskExecutor.submit(() -> { taskProcessor.process((T) task); return null; }); @@ -137,7 +138,8 @@ public class HousekeeperService { if (e instanceof ExecutionException) { error = e.getCause(); } else if (e instanceof TimeoutException) { - error = new TimeoutException("Timeout after " + config.getTaskProcessingTimeout() + " seconds"); + future.cancel(true); // interrupting the task + error = new TimeoutException("Timeout after " + config.getTaskProcessingTimeout() + " ms"); } if (msg.getTask().getAttempt() < config.getMaxReprocessingAttempts()) { diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java index f193b154db..5ef8b2f7f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/HousekeeperTaskProcessor.java @@ -20,6 +20,8 @@ import org.thingsboard.server.common.data.housekeeper.HousekeeperTask; import org.thingsboard.server.common.data.housekeeper.HousekeeperTaskType; import org.thingsboard.server.common.msg.housekeeper.HousekeeperClient; +import java.util.concurrent.Future; + public abstract class HousekeeperTaskProcessor { @Autowired @@ -29,4 +31,13 @@ public abstract class HousekeeperTaskProcessor { public abstract HousekeeperTaskType getTaskType(); + public V wait(Future future) throws Exception { + try { + return future.get(); // will be interrupted after taskProcessingTimeout + } catch (InterruptedException e) { + future.cancel(true); // interrupting the underlying task + throw e; + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java index 1f4b6b57ae..64d0043c5a 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/processor/LatestTsDeletionTaskProcessor.java @@ -33,7 +33,7 @@ public class LatestTsDeletionTaskProcessor extends HousekeeperTaskProcessor { + Future future = someExecutor.submit(() -> { + try { + Thread.sleep(TimeUnit.HOURS.toMillis(24)); + } catch (InterruptedException e) { + underlyingTaskInterrupted.set(true); + } + }); + try { + future.get(); + } catch (InterruptedException e) { + taskInterrupted.set(true); + future.cancel(true); + throw e; + } + return null; + }).when(tsHistoryDeletionTaskProcessor).process(any()); + + Device device = createDevice("test", "test"); + createRelatedData(device.getId()); + + doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); + + int attempts = 2; + await().atMost(30, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { + for (int i = 0; i <= attempts; i++) { + int attempt = i; + verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, + task -> task.getAttempt() == attempt)), argThat(error -> error instanceof TimeoutException)); + } + }); + assertThat(taskInterrupted).isTrue(); + assertThat(underlyingTaskInterrupted).isTrue(); + + assertThat(getTimeseriesHistory(device.getId())).isNotEmpty(); + doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); + someExecutor.shutdown(); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(getTimeseriesHistory(device.getId())).isEmpty(); + }); + } + @Test public void whenReprocessingAttemptsExceeded_thenDropTheTask() throws Exception { TimeoutException error = new TimeoutException("Test timeout"); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java index fa00856b4b..50a109163c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/SemaphoreWithTbMsgQueue.java @@ -115,7 +115,7 @@ public class SemaphoreWithTbMsgQueue { } TbMsg msg = tbMsgTbContext.msg(); TbContext ctx = tbMsgTbContext.ctx(); - log.warn("[{}] Failed to process message: {}", entityId, msg, t); + log.debug("[{}] Failed to process message: {}", entityId, msg, t); ctx.tellFailure(msg, t); // you are not allowed to throw here, because queue will remain unprocessed continue; // We are probably the last who process the queue. We have to continue poll until get successful callback or queue is empty } From dd02e44822b59a984442361864f7a1d22e94c446 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 3 Dec 2024 12:59:24 +0200 Subject: [PATCH 06/10] Housekeeper: cancel task future in finally block --- .../server/service/housekeeper/HousekeeperService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java index c69bc62692..cfa5eeac7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java +++ b/application/src/main/java/org/thingsboard/server/service/housekeeper/HousekeeperService.java @@ -138,7 +138,6 @@ public class HousekeeperService { if (e instanceof ExecutionException) { error = e.getCause(); } else if (e instanceof TimeoutException) { - future.cancel(true); // interrupting the task error = new TimeoutException("Timeout after " + config.getTaskProcessingTimeout() + " ms"); } @@ -155,6 +154,10 @@ public class HousekeeperService { .build()); } statsService.ifPresent(statsService -> statsService.reportFailure(taskType, msg)); + } finally { + if (future != null && !future.isDone()) { + future.cancel(true); + } } } From 68bc840dbd2486b230000ba59e434877ef478fa7 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 3 Dec 2024 13:38:20 +0200 Subject: [PATCH 07/10] UI: Refactoring segmented button --- .../json/system/widget_bundles/buttons.json | 2 +- .../widget_bundles/control_widgets.json | 2 +- ...ed_button.json => two_segment_button.json} | 16 ++++++------- ...gmented-button-basic-config.component.html | 24 +++++++++---------- ...segmented-button-basic-config.component.ts | 2 +- .../button/segmented-button-widget.models.ts | 2 +- ... two-segment-button-widget.component.html} | 0 ... two-segment-button-widget.component.scss} | 0 ...=> two-segment-button-widget.component.ts} | 8 +++---- ...nted-button-widget-settings.component.html | 22 ++++++++--------- ...mented-button-widget-settings.component.ts | 2 +- .../widget/widget-components.module.ts | 6 ++--- ui-ngx/src/app/shared/shared.module.ts | 4 ++-- .../assets/locale/locale.constant-en_US.json | 22 ++++++++--------- 14 files changed, 56 insertions(+), 56 deletions(-) rename application/src/main/data/json/system/widget_types/{segmented_button.json => two_segment_button.json} (96%) rename ui-ngx/src/app/modules/home/components/widget/lib/button/{segmented-button-widget.component.html => two-segment-button-widget.component.html} (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/button/{segmented-button-widget.component.scss => two-segment-button-widget.component.scss} (100%) rename ui-ngx/src/app/modules/home/components/widget/lib/button/{segmented-button-widget.component.ts => two-segment-button-widget.component.ts} (93%) diff --git a/application/src/main/data/json/system/widget_bundles/buttons.json b/application/src/main/data/json/system/widget_bundles/buttons.json index aaed9f73e1..facca89336 100644 --- a/application/src/main/data/json/system/widget_bundles/buttons.json +++ b/application/src/main/data/json/system/widget_bundles/buttons.json @@ -11,7 +11,7 @@ "action_button", "command_button", "toggle_button", - "segmented_button", + "two_segment_button", "power_button" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index 6bc4bdc830..c3dc219f5b 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -11,7 +11,7 @@ "single_switch", "command_button", "toggle_button", - "segmented_button", + "two_segment_button", "power_button", "slider", "control_widgets.switch_control", diff --git a/application/src/main/data/json/system/widget_types/segmented_button.json b/application/src/main/data/json/system/widget_types/two_segment_button.json similarity index 96% rename from application/src/main/data/json/system/widget_types/segmented_button.json rename to application/src/main/data/json/system/widget_types/two_segment_button.json index 967ed82315..96c7987e13 100644 --- a/application/src/main/data/json/system/widget_types/segmented_button.json +++ b/application/src/main/data/json/system/widget_types/two_segment_button.json @@ -1,15 +1,15 @@ { - "fqn": "segmented_button", - "name": "Segmented button", + "fqn": "two_segment_button", + "name": "Two segment button", "deprecated": false, - "image": "tb-image;/api/images/system/segmented-button.svg", + "image": "tb-image;/api/images/system/two-segment-button.svg", "description": "Facilitates navigation to other dashboards, states, or custom actions. Configurable settings allow for on-click action definition and conditions for button activation or deactivation. It offers various layouts and custom styling options for different stat", "descriptor": { "type": "latest", "sizeX": 5, "sizeY": 1.5, "resources": [], - "templateHtml": "\n", + "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n maxDatasources: 1,\n maxDataKeys: 0,\n singleEntity: true,\n previewWidth: '300px',\n previewHeight: '80px',\n embedTitlePanel: true,\n overflowVisible: true,\n hideDataSettings: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", "settingsSchema": "", @@ -21,12 +21,12 @@ }, "resources": [ { - "link": "/api/images/system/segmented-button.svg", - "title": "\"Segmented button\" system widget image", + "link": "/api/images/system/two-segment-button.svg", + "title": "\"Two segment button\" system widget image", "type": "IMAGE", "subType": "IMAGE", - "fileName": "segmented-button.svg", - "publicResourceKey": "koboT6A2CqNyRYPTP3ENmjMM6P8KK1PP", + "fileName": "two-segment-button.svg", + "publicResourceKey": "FmYUNvspfrtAsh4lx8vu1fgDA1wBjoC9", "mediaType": "image/svg+xml", "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgZmlsbD0ibm9uZSIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPHJlY3QgeD0iLjM4IiB5PSI1OC4yIiB3aWR0aD0iMTk5LjI1IiBoZWlnaHQ9IjQzLjYxIiByeD0iMi42MyIgc3Ryb2tlPSIjMzA1NjgwIiBzdHJva2Utd2lkdGg9Ii43NSIvPgogPHJlY3QgeD0iMy4wMSIgeT0iNjAuODMiIHdpZHRoPSI5Ni45OSIgaGVpZ2h0PSIzOC4zNSIgcng9IjEuNSIgZmlsbD0iIzMwNTY4MCIvPgogPHBhdGggZD0iTTI5LjI4IDc1LjUyVjgzSDI4di03LjQ4aDEuMjh6bTIuMzUgMHYxLjAyaC01Ljk2di0xLjAyaDUuOTZ6bTEuOTIgMi45OFY4M2gtMS4yM3YtNS41NmgxLjE4bC4wNSAxLjA2em0xLjctMS4xdjEuMTVhMi40NCAyLjQ0IDAgMCAwLTEuMDcuMDYgMS4wNiAxLjA2IDAgMCAwLS42NS42NWMtLjA2LjE2LS4xLjM0LS4xLjUzbC0uMjkuMDJjMC0uMzUuMDQtLjY3LjEtLjk3LjA3LS4zLjE4LS41Ni4zMS0uNzguMTQtLjIzLjMyLS40LjUzLS41M2ExLjQgMS40IDAgMCAxIC45OC0uMTdsLjIuMDR6bTMuOTIgNC40OHYtMi42NWMwLS4yLS4wNC0uMzctLjEtLjUxYS43Ni43NiAwIDAgMC0uMzQtLjM0IDEuMTIgMS4xMiAwIDAgMC0uNTQtLjExYy0uMiAwLS4zOC4wMy0uNTMuMWEuODUuODUgMCAwIDAtLjM0LjI4LjY3LjY3IDAgMCAwLS4xMi40aC0xLjI0YzAtLjIzLjA2LS40NC4xNi0uNjUuMS0uMi4yNi0uMzguNDYtLjU1LjItLjE2LjQ1LS4yOC43My0uMzguMjgtLjA5LjYtLjEzLjk0LS4xMy40MiAwIC44LjA3IDEuMTIuMi4zMi4xNS41OC4zNi43Ni42NC4xOS4yOC4yOC42NC4yOCAxLjA2djIuNDdjMCAuMjYuMDIuNDkuMDYuNjkuMDMuMi4wOS4zNy4xNS41MlY4M2gtMS4yN2EyLjIgMi4yIDAgMCAxLS4xMy0uNWMtLjAzLS4yMi0uMDUtLjQyLS4wNS0uNjJ6bS4xOC0yLjI2LjAxLjc2aC0uODljLS4yMyAwLS40My4wMy0uNi4wNy0uMTguMDQtLjMzLjEtLjQ0LjE5cy0uMi4xOC0uMjYuM2EuODcuODcgMCAwIDAtLjEuMzljMCAuMTUuMDQuMjguMTEuNC4wNy4xMy4xNy4yMi4zLjI5LjEzLjA3LjMuMS40OC4xYTEuMzYgMS4zNiAwIDAgMCAxLjEyLS41NGMuMS0uMTUuMTctLjMuMTctLjQ0bC40LjU1Yy0uMDQuMTQtLjEuMy0uMi40Ni0uMS4xNi0uMjQuMzEtLjQuNDZhMS45NCAxLjk0IDAgMCAxLTEuMzMuNWMtLjM2IDAtLjY5LS4wOC0uOTctLjIyLS4yOS0uMTUtLjUtLjM1LS42Ny0uNmExLjc3IDEuNzcgMCAwIDEtLjA4LTEuNjNjLjEyLS4yMi4yOC0uNDIuNS0uNTcuMjItLjE1LjQ4LS4yNy44LS4zNS4zMS0uMDguNjctLjEyIDEuMDgtLjEyaC45N3ptNS45NCAyLjIzVjc1LjFoMS4yNFY4M2gtMS4xMmwtLjEyLTEuMTV6bS0zLjYyLTEuNTd2LS4xYzAtLjQzLjA1LS44MS4xNS0xLjE2LjEtLjM1LjI0LS42NS40My0uOWExLjkgMS45IDAgMCAxIDEuNi0uNzggMS44NCAxLjg0IDAgMCAxIDEuNTMuNzZjLjE4LjIzLjMzLjUyLjQzLjg1LjExLjM0LjE4LjcuMjMgMS4xMXYuMzVjLS4wNS40LS4xMi43Ni0uMjMgMS4wOS0uMS4zMy0uMjUuNjEtLjQyLjg1YTEuODQgMS44NCAwIDAgMS0xLjU1Ljc1IDEuOTUgMS45NSAwIDAgMS0xLjU5LS44IDIuNzkgMi43OSAwIDAgMS0uNDMtLjljLS4xLS4zNC0uMTUtLjcyLS4xNS0xLjEyem0xLjI0LS4xdi4xYzAgLjI1LjAyLjUuMDcuNzEuMDQuMjIuMTIuNDEuMjIuNTguMS4xNy4yMi4zLjM4LjQuMTYuMDguMzYuMTMuNTguMTMuMjggMCAuNTEtLjA2LjctLjE4cy4zMi0uMy40Mi0uNWMuMS0uMjIuMTgtLjQ1LjIxLS43MXYtLjkzYy0uMDItLjItLjA2LS40LS4xMi0uNTdhMS41MSAxLjUxIDAgMCAwLS4yNi0uNDYgMS4yNCAxLjI0IDAgMCAwLS45NS0uNDJjLS4yMiAwLS40MS4wNS0uNTcuMTUtLjE1LjEtLjI4LjIyLS4zOS40LS4xLjE2LS4xNy4zNi0uMjIuNThzLS4wNy40Ni0uMDcuNzF6bTYuNDMtMi43NFY4M0g0OC4xdi01LjU2aDEuMjR6bS0xLjMyLTEuNDZjMC0uMTkuMDYtLjM1LjE4LS40N2EuNy43IDAgMCAxIC41My0uMTljLjIxIDAgLjM5LjA2LjUxLjIuMTMuMTEuMi4yNy4yLjQ2IDAgLjE4LS4wNy4zNC0uMi40NmEuNzEuNzEgMCAwIDEtLjUxLjE5LjcyLjcyIDAgMCAxLS41My0uMTkuNjMuNjMgMCAwIDEtLjE4LS40NnptNS40MiAxLjQ2di45SDUwLjN2LS45aDMuMTR6bS0yLjIzLTEuMzZoMS4yM3Y1LjM4YzAgLjE3LjAzLjMuMDguNC4wNS4wOS4xMi4xNS4yLjE4LjEuMDMuMi4wNC4zMi4wNGExLjkgMS45IDAgMCAwIC40NC0uMDR2Ljk0YTMuMTcgMy4xNyAwIDAgMS0uODIuMTJjLS4yOCAwLS41NC0uMDUtLjc2LS4xNS0uMjEtLjEtLjM5LS4yNy0uNS0uNWExLjk0IDEuOTQgMCAwIDEtLjItLjkxdi01LjQ2em00LjcgMS4zNlY4M2gtMS4yNXYtNS41NmgxLjI1em0tMS4zMy0xLjQ2YzAtLjE5LjA2LS4zNS4xOS0uNDdhLjcuNyAwIDAgMSAuNTItLjE5Yy4yMiAwIC40LjA2LjUyLjIuMTMuMTEuMTkuMjcuMTkuNDYgMCAuMTgtLjA2LjM0LS4yLjQ2YS43MS43MSAwIDAgMS0uNTEuMTkuNzIuNzIgMCAwIDEtLjUyLS4xOS42My42MyAwIDAgMS0uMTktLjQ2em0yLjYzIDQuM3YtLjEyYzAtLjQuMDYtLjc3LjE4LTEuMTEuMTItLjM1LjI4LS42NS41LS45LjIzLS4yNi41LS40Ni44Mi0uNi4zMi0uMTQuNjgtLjIxIDEuMDktLjIxLjQgMCAuNzcuMDcgMS4wOC4yMS4zMy4xNC42LjM0LjgyLjYuMjIuMjUuNC41NS41MS45LjEyLjM0LjE4LjcxLjE4IDEuMTF2LjEyYzAgLjQtLjA2Ljc3LS4xOCAxLjEyYTIuMzYgMi4zNiAwIDAgMS0yLjQgMS43Yy0uNDEgMC0uNzgtLjA3LTEuMS0uMmEyLjM2IDIuMzYgMCAwIDEtLjgxLS42Yy0uMjItLjI2LS40LS41Ni0uNTEtLjlhMy40NCAzLjQ0IDAgMCAxLS4xOC0xLjEyem0xLjI0LS4xMnYuMTJjMCAuMjUuMDMuNDkuMDguNzEuMDUuMjIuMTMuNDIuMjQuNTkuMTEuMTYuMjUuMy40Mi40LjE4LjA5LjM4LjE0LjYyLjE0YTEuMTggMS4xOCAwIDAgMCAxLjAxLS41NGMuMTEtLjE3LjItLjM3LjI0LS41OS4wNi0uMjIuMDktLjQ2LjA5LS43di0uMTNjMC0uMjQtLjAzLS40OC0uMDktLjdhMS44IDEuOCAwIDAgMC0uMjQtLjU5IDEuMTggMS4xOCAwIDAgMC0xLjAyLS41NWMtLjI0IDAtLjQ0LjA1LS42MS4xNS0uMTcuMS0uMy4yMy0uNDIuNC0uMS4xNy0uMTkuMzctLjI0LjYtLjA1LjIxLS4wOC40NS0uMDguN3ptNi4zOS0xLjUzVjgzSDYzLjZ2LTUuNTZoMS4xN2wuMDcgMS4xOXpNNjQuNjIgODBoLS40YzAtLjQuMDYtLjc2LjE2LTEuMDkuMTEtLjMzLjI2LS42LjQ2LS44NGEyIDIgMCAwIDEgMS42LS43NGMuMjcgMCAuNS4wMy43Mi4xLjIyLjA4LjQuMi41Ni4zNi4xNi4xNy4yOC4zOC4zNi42NS4wOS4yNi4xMy41OC4xMy45NlY4M2gtMS4yNXYtMy42YzAtLjI3LS4wNC0uNDgtLjEyLS42M2EuNjYuNjYgMCAwIDAtLjMzLS4zM2MtLjE0LS4wNy0uMzItLjEtLjU0LS4xYTEuMiAxLjIgMCAwIDAtMSAuNWMtLjEuMTUtLjIuMzItLjI2LjUzLS4wNi4yLS4xLjQxLS4xLjY0em04LjE3IDEuODd2LTIuNjVjMC0uMi0uMDMtLjM3LS4xLS41MWEuNzYuNzYgMCAwIDAtLjMzLS4zNCAxLjEyIDEuMTIgMCAwIDAtLjU1LS4xMWMtLjIgMC0uMzcuMDMtLjUyLjFhLjg1Ljg1IDAgMCAwLS4zNS4yOC42Ny42NyAwIDAgMC0uMTIuNEg2OS42YzAtLjIzLjA1LS40NC4xNi0uNjUuMS0uMi4yNi0uMzguNDYtLjU1LjItLjE2LjQ0LS4yOC43Mi0uMzguMjgtLjA5LjYtLjEzLjk1LS4xMy40MiAwIC43OS4wNyAxLjExLjIuMzMuMTUuNTguMzYuNzcuNjQuMTguMjguMjguNjQuMjggMS4wNnYyLjQ3YzAgLjI2LjAyLjQ5LjA1LjY5LjA0LjIuMDkuMzcuMTYuNTJWODNoLTEuMjdhMi4yIDIuMiAwIDAgMS0uMTQtLjVjLS4wMy0uMjItLjA1LS40Mi0uMDUtLjYyem0uMTgtMi4yNi4wMS43NmgtLjg4Yy0uMjMgMC0uNDQuMDMtLjYxLjA3LS4xOC4wNC0uMzIuMS0uNDQuMTlzLS4yLjE4LS4yNi4zYS44Ny44NyAwIDAgMC0uMDkuMzljMCAuMTUuMDQuMjguMS40LjA3LjEzLjE3LjIyLjMuMjkuMTQuMDcuMy4xLjQ5LjFhMS4zNiAxLjM2IDAgMCAwIDEuMTEtLjU0Yy4xMS0uMTUuMTctLjMuMTgtLjQ0bC40LjU1YTIuMiAyLjIgMCAwIDEtLjYuOTEgMS45NCAxLjk0IDAgMCAxLTEuMzQuNWMtLjM2IDAtLjY4LS4wNy0uOTctLjIxLS4yOC0uMTUtLjUtLjM1LS42Ni0uNmExLjc3IDEuNzcgMCAwIDEtLjA4LTEuNjNjLjExLS4yMi4yOC0uNDIuNS0uNTcuMjEtLjE1LjQ4LS4yNy44LS4zNS4zLS4wOC42Ny0uMTIgMS4wNy0uMTJoLjk3em0zLjg4LTQuNTJWODNINzUuNnYtNy45aDEuMjV6IiBmaWxsPSIjZmZmIi8+CiA8cGF0aCBkPSJNMTM2Ljg2IDc4LjY0djEuMDJoLTMuOTd2LTEuMDJoMy45N3ptLTMuNjUtMy4xMlY4M2gtMS4zdi03LjQ4aDEuM3ptNC42NCAwVjgzaC0xLjI4di03LjQ4aDEuMjh6bTIuOTMgMS45MlY4M2gtMS4yNHYtNS41NmgxLjI0em0tMS4zMy0xLjQ2YzAtLjE5LjA3LS4zNS4xOS0uNDcuMTMtLjEzLjMtLjE5LjUyLS4xOS4yMiAwIC40LjA2LjUyLjIuMTMuMTEuMi4yNy4yLjQ2IDAgLjE4LS4wNy4zNC0uMi40NmEuNzEuNzEgMCAwIDEtLjUyLjE5LjcyLjcyIDAgMCAxLS41Mi0uMTkuNjMuNjMgMCAwIDEtLjE5LS40NnptNS4zNyAzLjMzdi45OWgtMi43MnYtMWgyLjcyem00LjI0LjloLTEuOTV2LTEuMDJoMS45NGMuMzQgMCAuNjItLjA2LjgzLS4xNy4yLS4xLjM2LS4yNi40Ni0uNDUuMS0uMi4xNC0uNDIuMTQtLjY3IDAtLjI0LS4wNS0uNDYtLjE0LS42Ni0uMS0uMjEtLjI1LS4zOC0uNDYtLjVzLS40OS0uMi0uODItLjJoLTEuNTZWODNoLTEuMjl2LTcuNDhoMi44NWMuNTcgMCAxLjA3LjEgMS40Ny4zLjQuMi43Mi40OS45My44NS4yMS4zNS4zMi43Ni4zMiAxLjIyIDAgLjQ4LS4xLjktLjMyIDEuMjQtLjIxLjM1LS41Mi42Mi0uOTMuOC0uNC4xOS0uOS4yOC0xLjQ3LjI4em02LjMyIDIuOWMtLjQxIDAtLjc4LS4wNy0xLjExLS4yYTIuNDYgMi40NiAwIDAgMS0xLjM4LTEuNDQgMy4wMSAzLjAxIDAgMCAxLS4xOC0xLjA2di0uMmMwLS40NC4wNi0uODQuMTktMS4xOS4xMi0uMzUuMy0uNjUuNTMtLjkuMjItLjI2LjQ5LS40NS44LS41OC4zLS4xNC42NC0uMiAxLS4yLjQgMCAuNzUuMDYgMS4wNC4yLjMuMTMuNTUuMzIuNzQuNTYuMi4yNC4zNS41My40NS44Ni4xLjMzLjE1LjcuMTUgMS4xdi41M2gtNC4zdi0uODloMy4wN3YtLjFjMC0uMjItLjA1LS40My0uMTMtLjYyLS4wOC0uMi0uMi0uMzYtLjM3LS40OGExLjEgMS4xIDAgMCAwLS42NS0uMThjLS4yMSAwLS40LjA1LS41NS4xNC0uMTYuMDgtLjMuMi0uNC4zN3MtLjE5LjM2LS4yNS42YTMuNCAzLjQgMCAwIDAtLjA4Ljc3di4yYzAgLjI1LjAzLjQ4LjEuNjkuMDcuMi4xNy4zOC4zLjU0YTEuMzQgMS4zNCAwIDAgMCAxLjEuNWMuMyAwIC41Ny0uMDcuOC0uMTlzLjQzLS4yOS42LS41bC42Ni42MmEyLjM0IDIuMzQgMCAwIDEtMS4xNy44OWMtLjI4LjEtLjYuMTUtLjk2LjE1ek0xNjAgNzguNVY4M2gtMS4yNHYtNS41NmgxLjE5bC4wNSAxLjA2em0xLjctMS4xdjEuMTVhMi40NyAyLjQ3IDAgMCAwLTEuMDcuMDYgMS4wNSAxLjA1IDAgMCAwLS42NS42NWMtLjA2LjE2LS4xLjM0LS4xLjUzbC0uMjkuMDJjMC0uMzUuMDQtLjY3LjEtLjk3LjA3LS4zLjE4LS41Ni4zMS0uNzguMTQtLjIzLjMyLS40LjUzLS41M2ExLjQgMS40IDAgMCAxIC45Ny0uMTdsLjIuMDR6bTIuNzQgNS42aC0xLjI0di02LjFjMC0uNDEuMDgtLjc2LjIzLTEuMDQuMTYtLjI4LjM4LS41LjY3LS42NGEyLjU4IDIuNTggMCAwIDEgMS43NS0uMTNsLS4wMy45NmExLjA0IDEuMDQgMCAwIDAtLjk3LjA2LjcyLjcyIDAgMCAwLS4zMS4zYy0uMDcuMTMtLjEuMy0uMS41Vjgzem0xLjE0LTUuNTZ2LjloLTMuMjR2LS45aDMuMjR6IiBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9Ii43NiIvPgo8L3N2Zz4K", "public": true diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html index 267a61f83b..54f9cd6625 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.html @@ -27,12 +27,12 @@
widgets.action-button.behavior
-
widgets.button-state.initial-state
+
widgets.button-state.selected-state
-
widgets.action-button.left-button-click
+
widgets.action-button.first-button-click
-
widgets.action-button.right-button-click
+
widgets.action-button.second-button-click
@@ -110,11 +110,11 @@
widgets.segmented-button.button-appearance
- {{ 'widgets.segmented-button.left' | translate }} - {{ 'widgets.segmented-button.right' | translate }} + {{ 'widgets.segmented-button.first' | translate }} + {{ 'widgets.segmented-button.second' | translate }}
-
+
{{ 'widgets.button.label' | translate }} @@ -145,7 +145,7 @@
-
+
{{ 'widgets.button.label' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts index 2b86d88a4a..cf9c830fe8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/segmented-button-basic-config.component.ts @@ -49,7 +49,7 @@ export class SegmentedButtonBasicConfigComponent extends BasicWidgetConfigCompon return getTargetDeviceFromDatasources(datasources); } - segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'left'; + segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'first'; segmentedButtonColorStylesType: SegmentedButtonColorStylesType = 'selected'; widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts index 611812f244..3c0417a406 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.models.ts @@ -32,7 +32,7 @@ export enum SegmentedButtonLayout { rounded = 'rounded' } -export type SegmentedButtonAppearanceType = 'left' | 'right'; +export type SegmentedButtonAppearanceType = 'first' | 'second'; export type SegmentedButtonColorStylesType = 'selected' | 'unselected'; export const segmentedButtonLayouts = Object.keys(SegmentedButtonLayout) as SegmentedButtonLayout[]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.html diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.scss similarity index 100% rename from ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.scss diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.ts similarity index 93% rename from ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.ts index 6c7ee3eef8..042f1518b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/button/segmented-button-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/button/two-segment-button-widget.component.ts @@ -28,12 +28,12 @@ import { ComponentStyle } from '@shared/models/widget-settings.models'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; @Component({ - selector: 'tb-segmented-button-widget', - templateUrl: './segmented-button-widget.component.html', - styleUrls: ['../action/action-widget.scss', './segmented-button-widget.component.scss'], + selector: 'tb-two-segment-button-widget', + templateUrl: './two-segment-button-widget.component.html', + styleUrls: ['../action/action-widget.scss', './two-segment-button-widget.component.scss'], encapsulation: ViewEncapsulation.None }) -export class SegmentedButtonWidgetComponent extends +export class TwoSegmentButtonWidgetComponent extends BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { settings: SegmentedButtonWidgetSettings; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html index 4cf82918cb..e259919cc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.html @@ -19,11 +19,11 @@
widgets.action-button.behavior
-
widgets.button-state.initial-state
+
widgets.button-state.selected-state
-
widgets.action-button.left-button-click
+
widgets.action-button.first-button-click
-
widgets.action-button.right-button-click
+
widgets.action-button.second-button-click
@@ -102,11 +102,11 @@
widgets.segmented-button.button-appearance
- {{ 'widgets.segmented-button.left' | translate }} - {{ 'widgets.segmented-button.right' | translate }} + {{ 'widgets.segmented-button.first' | translate }} + {{ 'widgets.segmented-button.second' | translate }}
-
+
{{ 'widgets.button.label' | translate }} @@ -137,7 +137,7 @@
-
+
{{ 'widgets.button.label' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts index 979cf2c078..7dab81ccad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/button/segmented-button-widget-settings.component.ts @@ -52,7 +52,7 @@ export class SegmentedButtonWidgetSettingsComponent extends WidgetSettingsCompon return this.segmentedButtonLayoutBorderMap.get(this.segmentedButtonWidgetSettingsForm.get('appearance.layout').value); } - segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'left'; + segmentedButtonAppearanceType: SegmentedButtonAppearanceType = 'first'; segmentedButtonColorStylesType: SegmentedButtonColorStylesType = 'selected'; widgetButtonToggleStates = Object.keys(WidgetButtonToggleState) as WidgetButtonToggleState[]; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 4488380804..21f2d89dc6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -87,7 +87,7 @@ import { } from '@home/components/widget/lib/cards/notification-type-filter-panel.component'; import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; -import { SegmentedButtonWidgetComponent } from '@home/components/widget/lib/button/segmented-button-widget.component'; +import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/button/two-segment-button-widget.component'; @NgModule({ declarations: [ @@ -125,7 +125,7 @@ import { SegmentedButtonWidgetComponent } from '@home/components/widget/lib/butt BarChartWithLabelsWidgetComponent, SingleSwitchWidgetComponent, ActionButtonWidgetComponent, - SegmentedButtonWidgetComponent, + TwoSegmentButtonWidgetComponent, CommandButtonWidgetComponent, PowerButtonWidgetComponent, SliderWidgetComponent, @@ -187,7 +187,7 @@ import { SegmentedButtonWidgetComponent } from '@home/components/widget/lib/butt BarChartWithLabelsWidgetComponent, SingleSwitchWidgetComponent, ActionButtonWidgetComponent, - SegmentedButtonWidgetComponent, + TwoSegmentButtonWidgetComponent, CommandButtonWidgetComponent, PowerButtonWidgetComponent, SliderWidgetComponent, diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 07eb13f25d..424332ac45 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -384,6 +384,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) WidgetsBundleSearchComponent, CopyButtonComponent, TogglePasswordComponent, + WidgetButtonToggleComponent, ProtobufContentComponent, BranchAutocompleteComponent, CountryAutocompleteComponent, @@ -420,7 +421,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) EmbedImageDialogComponent, ImageGalleryDialogComponent, WidgetButtonComponent, - WidgetButtonToggleComponent, HexInputComponent, ScadaSymbolInputComponent ], @@ -642,6 +642,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) WidgetsBundleSearchComponent, CopyButtonComponent, TogglePasswordComponent, + WidgetButtonToggleComponent, ProtobufContentComponent, BranchAutocompleteComponent, CountryAutocompleteComponent, @@ -678,7 +679,6 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) EmbedImageDialogComponent, ImageGalleryDialogComponent, WidgetButtonComponent, - WidgetButtonToggleComponent, ScadaSymbolInputComponent ] }) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4619b14112..3b35fe55e8 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5622,10 +5622,10 @@ "behavior": "Behavior", "on-click": "On click", "on-click-hint": "Action triggered when the button is clicked", - "left-button-click": "Left button click", - "left-button-click-hint": "Action while pressing on left button.", - "right-button-click": "Right button click", - "right-button-click-hint": "Action while pressing on right button.", + "first-button-click": "First button click", + "first-button-click-hint": "Action while pressing on first button.", + "second-button-click": "Second button click", + "second-button-click-hint": "Action while pressing on second button.", "button-click-hint": "Action while pressing on widget." }, "command-button": { @@ -5677,8 +5677,8 @@ "layout-rounded": "Rounded", "card-border": "Card border", "button-appearance": "Button appearance", - "left": "Left", - "right": "Right", + "first": "First", + "second": "Second", "color-styles": "Color styles", "selected": "Selected", "unselected": "Unselected" @@ -5710,16 +5710,16 @@ "activated-state-hint": "Configure condition under which the button is active.", "disabled-state": "Disabled state", "disabled-state-hint": "Configure condition under which the button is disabled.", - "initial-state": "Initial button", - "initial-state-hint": "Action to get the initial state of the component", + "selected-state": "Select button", + "selected-state-hint": "Configure condition under which the button is select.", "enabled": "Enabled", "hovered": "Hovered", "pressed": "Pressed", "activated": "Activated", "disabled": "Disabled", - "initial": "Left button", - "left": "Left", - "right": "Right" + "initial": "First button", + "first": "First", + "second": "Second" }, "background": { "background": "Background", From 8ab18b344e10f594dc4110e5d7c659c0d00e11f2 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 3 Dec 2024 13:49:31 +0200 Subject: [PATCH 08/10] UI: Refactoring shared import --- ui-ngx/src/app/shared/shared.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 424332ac45..7719dc683e 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -154,6 +154,7 @@ import { OtaPackageAutocompleteComponent } from '@shared/components/ota-package/ import { MAT_DATE_LOCALE } from '@angular/material/core'; import { CopyButtonComponent } from '@shared/components/button/copy-button.component'; import { TogglePasswordComponent } from '@shared/components/button/toggle-password.component'; +import { WidgetButtonToggleComponent } from '@shared/components/button/widget-button-toggle.component'; import { HelpPopupComponent } from '@shared/components/help-popup.component'; import { TbPopoverComponent, TbPopoverDirective } from '@shared/components/popover.component'; import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive'; @@ -218,7 +219,6 @@ import { CountryAutocompleteComponent } from '@shared/components/country-autocom import { CountryData } from '@shared/models/country.models'; import { SvgXmlComponent } from '@shared/components/svg-xml.component'; import { DatapointsLimitComponent } from '@shared/components/time/datapoints-limit.component'; -import { WidgetButtonToggleComponent } from '@shared/components/button/widget-button-toggle.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; From b39f087feca4ae26503c7cd6d721a6e801332889 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 3 Dec 2024 13:53:45 +0200 Subject: [PATCH 09/10] UI: Rename segmented button to Two-segment button --- .../main/data/json/system/widget_types/two_segment_button.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/data/json/system/widget_types/two_segment_button.json b/application/src/main/data/json/system/widget_types/two_segment_button.json index 96c7987e13..d32d139dce 100644 --- a/application/src/main/data/json/system/widget_types/two_segment_button.json +++ b/application/src/main/data/json/system/widget_types/two_segment_button.json @@ -1,6 +1,6 @@ { "fqn": "two_segment_button", - "name": "Two segment button", + "name": "Two-segment button", "deprecated": false, "image": "tb-image;/api/images/system/two-segment-button.svg", "description": "Facilitates navigation to other dashboards, states, or custom actions. Configurable settings allow for on-click action definition and conditions for button activation or deactivation. It offers various layouts and custom styling options for different stat", From 0a11b47a7bcf88c2aa94721ad2da58384243ee1f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 3 Dec 2024 14:10:41 +0200 Subject: [PATCH 10/10] license:format --- .../util/ThingsBoardScheduledThreadPoolExecutor.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardScheduledThreadPoolExecutor.java b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardScheduledThreadPoolExecutor.java index fd3ecb085c..831982e102 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardScheduledThreadPoolExecutor.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardScheduledThreadPoolExecutor.java @@ -1,12 +1,12 @@ /** * 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 - *

+ * + * 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.