From 0177904eb20a8bf221e86afc79686d780b1a9686 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 1 Apr 2022 19:18:50 +0300 Subject: [PATCH] UI: Add markdown widget settings form --- .../json/system/widget_bundles/cards.json | 5 +- .../markdown-widget-settings.component.html | 38 +++++ .../markdown-widget-settings.component.ts | 76 +++++++++ .../lib/settings/widget-settings.module.ts | 12 +- .../app/shared/components/css.component.scss | 3 +- .../shared/components/js-func.component.html | 8 +- .../shared/components/js-func.component.scss | 22 ++- .../components/markdown-editor.component.html | 47 ++++++ .../components/markdown-editor.component.scss | 77 +++++++++ .../components/markdown-editor.component.ts | 154 ++++++++++++++++++ ui-ngx/src/app/shared/shared.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 8 + 12 files changed, 438 insertions(+), 15 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts create mode 100644 ui-ngx/src/app/shared/components/markdown-editor.component.html create mode 100644 ui-ngx/src/app/shared/components/markdown-editor.component.scss create mode 100644 ui-ngx/src/app/shared/components/markdown-editor.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index dafd87b14b..e574e1d98e 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -186,8 +186,9 @@ "templateHtml": "\n", "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Markdown/HTML card\",\n \"properties\": {\n \"markdownTextPattern\": {\n \"title\": \"Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')\",\n \"type\": \"string\",\n \"default\": \"# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.\"\n },\n \"markdownCss\": {\n \"title\": \"Markdown/HTML CSS\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useMarkdownTextFunction\": {\n \"title\": \"Use markdown/HTML value function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"markdownTextFunction\": {\n \"title\": \"Markdown/HTML value function: f(data)\",\n \"type\": \"string\",\n \"default\": \"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useMarkdownTextFunction\",\n {\n \"key\": \"markdownTextPattern\",\n \"type\": \"markdown\",\n \"condition\": \"model.useMarkdownTextFunction !== true\"\n },\n {\n \"key\": \"markdownTextFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/markdown/markdown_text_fn\",\n \"condition\": \"model.useMarkdownTextFunction === true\"\n },\n {\n \"key\": \"markdownCss\",\n \"type\": \"css\"\n }\n ]\n}\n", - "dataKeySettingsSchema": "{}\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-markdown-widget-settings", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" } }, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html new file mode 100644 index 0000000000..f586552d15 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.html @@ -0,0 +1,38 @@ + +
+ + {{ 'widgets.markdown.use-markdown-text-function' | translate }} + +
+ + +
+
+ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts new file mode 100644 index 0000000000..db78d8b42f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/markdown-widget-settings.component.ts @@ -0,0 +1,76 @@ +/// +/// Copyright © 2016-2022 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 { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; + +@Component({ + selector: 'tb-markdown-widget-settings', + templateUrl: './markdown-widget-settings.component.html', + styleUrls: ['./widget-settings.scss'] +}) +export class MarkdownWidgetSettingsComponent extends WidgetSettingsComponent { + + markdownWidgetSettingsForm: FormGroup; + + constructor(protected store: Store, + private fb: FormBuilder) { + super(store); + } + + protected settingsForm(): FormGroup { + return this.markdownWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return { + useMarkdownTextFunction: false, + markdownTextPattern: '# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.', + markdownTextFunction: 'return \'# Some title\\\\n - Entity name: \' + data[0][\'entityName\'];', + markdownCss: '' + }; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.markdownWidgetSettingsForm = this.fb.group({ + useMarkdownTextFunction: [settings.useMarkdownTextFunction, []], + markdownTextPattern: [settings.markdownTextPattern, []], + markdownTextFunction: [settings.qrCodeTextFunction, []], + markdownCss: [settings.markdownCss, []] + }); + } + + protected validatorTriggers(): string[] { + return ['useMarkdownTextFunction']; + } + + protected updateValidators(emitEvent: boolean) { + const useMarkdownTextFunction: boolean = this.markdownWidgetSettingsForm.get('useMarkdownTextFunction').value; + if (useMarkdownTextFunction) { + this.markdownWidgetSettingsForm.get('markdownTextPattern').disable(); + this.markdownWidgetSettingsForm.get('markdownTextFunction').enable(); + } else { + this.markdownWidgetSettingsForm.get('markdownTextPattern').enable(); + this.markdownWidgetSettingsForm.get('markdownTextFunction').disable(); + } + this.markdownWidgetSettingsForm.get('markdownTextPattern').updateValueAndValidity({emitEvent}); + this.markdownWidgetSettingsForm.get('markdownTextFunction').updateValueAndValidity({emitEvent}); + } + +} 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 e31eef1f87..03e04bed31 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 @@ -29,13 +29,17 @@ import { import { TimeseriesTableLatestKeySettingsComponent } from '@home/components/widget/lib/settings/timeseries-table-latest-key-settings.component'; +import { + MarkdownWidgetSettingsComponent +} from '@home/components/widget/lib/settings/markdown-widget-settings.component'; @NgModule({ declarations: [ QrCodeWidgetSettingsComponent, TimeseriesTableWidgetSettingsComponent, TimeseriesTableKeySettingsComponent, - TimeseriesTableLatestKeySettingsComponent + TimeseriesTableLatestKeySettingsComponent, + MarkdownWidgetSettingsComponent ], imports: [ CommonModule, @@ -46,7 +50,8 @@ import { QrCodeWidgetSettingsComponent, TimeseriesTableWidgetSettingsComponent, TimeseriesTableKeySettingsComponent, - TimeseriesTableLatestKeySettingsComponent + TimeseriesTableLatestKeySettingsComponent, + MarkdownWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -56,5 +61,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type -
@@ -37,10 +37,10 @@
-
+
-
- +
+
diff --git a/ui-ngx/src/app/shared/components/js-func.component.scss b/ui-ngx/src/app/shared/components/js-func.component.scss index 3a7ddc3d09..6b11058333 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.scss +++ b/ui-ngx/src/app/shared/components/js-func.component.scss @@ -24,14 +24,22 @@ height: 100%; } + &:not(.tb-js-func-title) { + .tb-js-func-panel { + margin-left: 15px; + } + } + + &.tb-js-func-title { + .tb-js-func-panel { + height: calc(100% - 40px); + } + } + .tb-js-func-panel { height: calc(100% - 80px); border: 1px solid #c0c0c0; - &:not(.tb-js-func-title) { - margin-left: 15px; - } - #tb-javascript-input { width: 100%; min-width: 200px; @@ -43,6 +51,12 @@ } } + &:not(.tb-fullscreen) { + &.tb-js-func-title { + padding-bottom: 15px; + } + } + .tb-js-func-toolbar { & > * { &:not(:last-child) { diff --git a/ui-ngx/src/app/shared/components/markdown-editor.component.html b/ui-ngx/src/app/shared/components/markdown-editor.component.html new file mode 100644 index 0000000000..e31ad8bc3f --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown-editor.component.html @@ -0,0 +1,47 @@ + +
+
+ +
+
+
+ + + +
+
+
+ +
+
+
+ +
+
diff --git a/ui-ngx/src/app/shared/components/markdown-editor.component.scss b/ui-ngx/src/app/shared/components/markdown-editor.component.scss new file mode 100644 index 0000000000..a4149c54d7 --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown-editor.component.scss @@ -0,0 +1,77 @@ +/** + * Copyright © 2016-2022 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. + */ +.markdown-content { + min-width: 400px; + &.tb-edit-mode { + .tb-markdown-view-container { + border: 1px solid #c0c0c0; + } + .tb-markdown-view { + padding: 20px; + } + &:not(.tb-fullscreen) { + padding-bottom: 15px; + .markdown-content-editor { + min-height: 200px; + max-height: 200px; + height: 200px; + } + .tb-markdown-view-container { + min-height: 200px; + max-height: 200px; + height: 200px; + } + } + } + &.tb-fullscreen { + background: #fff; + .markdown-content-editor { + height: calc(100% - 40px); + } + } + .markdown-content-editor { + position: relative; + height: 100%; + } + .tb-markdown-editor { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px solid #c0c0c0; + } + .tb-markdown-view-container { + overflow: auto; + height: 100%; + } + .buttons-panel { + position: absolute; + top: 5px; + right: 24px; + z-index: 1; + button.edit-toggle { + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + } + } +} diff --git a/ui-ngx/src/app/shared/components/markdown-editor.component.ts b/ui-ngx/src/app/shared/components/markdown-editor.component.ts new file mode 100644 index 0000000000..7c1c9c7c02 --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown-editor.component.ts @@ -0,0 +1,154 @@ +/// +/// Copyright © 2016-2022 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, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Ace } from 'ace-builds'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { getAce } from '@shared/models/ace/ace.models'; + +@Component({ + selector: 'tb-markdown-editor', + templateUrl: './markdown-editor.component.html', + styleUrls: ['./markdown-editor.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MarkdownEditorComponent), + multi: true + } + ] +}) +export class MarkdownEditorComponent implements OnInit, ControlValueAccessor { + + @Input() label: string; + + @Input() disabled: boolean; + + @Input() readonly: boolean; + + @ViewChild('markdownEditor', {static: true}) + markdownEditorElmRef: ElementRef; + + private markdownEditor: Ace.Editor; + + editorMode = true; + + fullscreen = false; + + markdownValue: string; + renderValue: string; + + ignoreChange = false; + + private propagateChange = null; + + private requiredValue: boolean; + + get required(): boolean { + return this.requiredValue; + } + + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + constructor() { + } + + ngOnInit(): void { + if (!this.readonly) { + const editorElement = this.markdownEditorElmRef.nativeElement; + let editorOptions: Partial = { + mode: 'ace/mode/markdown', + showGutter: true, + showPrintMargin: false, + readOnly: false + }; + + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + + getAce().subscribe( + (ace) => { + this.markdownEditor = ace.edit(editorElement, editorOptions); + this.markdownEditor.session.setUseWrapMode(false); + this.markdownEditor.setValue(this.markdownValue ? this.markdownValue : '', -1); + this.markdownEditor.on('change', () => { + if (!this.ignoreChange) { + this.updateView(); + } + }); + } + ); + + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: string): void { + this.editorMode = true; + this.markdownValue = value; + this.renderValue = this.markdownValue ? this.markdownValue : ' '; + if (this.markdownEditor) { + this.ignoreChange = true; + this.markdownEditor.setValue(this.markdownValue ? this.markdownValue : '', -1); + this.ignoreChange = false; + } + } + + updateView() { + const editorValue = this.markdownEditor.getValue(); + if (this.markdownValue !== editorValue) { + this.markdownValue = editorValue; + this.renderValue = this.markdownValue ? this.markdownValue : ' '; + this.propagateChange(this.markdownValue); + } + } + + onFullscreen() { + if (this.markdownEditor) { + setTimeout(() => { + this.markdownEditor.resize(); + }, 0); + } + } + + toggleEditMode() { + this.editorMode = !this.editorMode; + if (this.editorMode && this.markdownEditor) { + setTimeout(() => { + this.markdownEditor.resize(); + }, 0); + } + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index a356fa5921..9d3d6864fe 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -79,6 +79,7 @@ import { EnumToArrayPipe } from '@shared/pipe/enum-to-array.pipe'; import { ClipboardModule } from 'ngx-clipboard'; import { ValueInputComponent } from '@shared/components/value-input.component'; import { MarkdownModule, MarkedOptions } from 'ngx-markdown'; +import { MarkdownEditorComponent } from '@shared/components/markdown-editor.component'; import { FullscreenDirective } from '@shared/components/fullscreen.directive'; import { HighlightPipe } from '@shared/pipe/highlight.pipe'; import { DashboardAutocompleteComponent } from '@shared/components/dashboard-autocomplete.component'; @@ -258,6 +259,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) KeyValMapComponent, NavTreeComponent, LedLightComponent, + MarkdownEditorComponent, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, @@ -452,6 +454,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) KeyValMapComponent, NavTreeComponent, LedLightComponent, + MarkdownEditorComponent, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, 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 47d22d9b3f..272ea90d7d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2390,6 +2390,8 @@ "error": "Login error" }, "markdown": { + "edit": "Edit", + "preview": "Preview", "copy-code": "Click to copy", "copied": "Copied!" }, @@ -3368,6 +3370,12 @@ "rotateButton": "Rotate polygon" } }, + "markdown": { + "use-markdown-text-function": "Use markdown/HTML value function", + "markdown-text-function": "Markdown/HTML value function", + "markdown-text-pattern": "Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')", + "markdown-css": "Markdown/HTML CSS" + }, "table": { "common-table-settings": "Common Table Settings", "enable-search": "Enable search",