From b9850d1f51d18d343363ab09a52f35c0172c4d2b Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 27 Feb 2026 11:05:58 +0200 Subject: [PATCH 01/10] UI: Implements rule chain note components --- ui-ngx/package.json | 2 +- .../app/core/services/item-buffer.service.ts | 40 ++- .../rulechain/add-note-dialog.component.html | 36 +++ .../rulechain/add-note-dialog.component.scss | 30 ++ .../pages/rulechain/rule-note.component.html | 46 +++ .../pages/rulechain/rule-note.component.ts | 57 ++++ .../rulechain/rulechain-page.component.html | 16 +- .../rulechain/rulechain-page.component.scss | 13 + .../rulechain/rulechain-page.component.ts | 288 +++++++++++++++--- .../pages/rulechain/rulechain-page.module.ts | 6 +- .../home/pages/rulechain/rulechain.module.ts | 10 +- .../pages/rulechain/rulenote.component.html | 47 +++ .../pages/rulechain/rulenote.component.scss | 71 +++++ .../pages/rulechain/rulenote.component.ts | 78 +++++ .../components/markdown-editor.component.html | 2 +- .../components/markdown-editor.component.ts | 2 + .../app/shared/models/rule-chain.models.ts | 3 +- .../src/app/shared/models/rule-node.models.ts | 10 +- .../help/en_US/rulechain/note_content.md | 195 ++++++++++++ .../assets/locale/locale.constant-en_US.json | 12 +- ui-ngx/yarn.lock | 3 +- 21 files changed, 909 insertions(+), 58 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.scss create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/rulenote.component.ts create mode 100644 ui-ngx/src/assets/help/en_US/rulechain/note_content.md diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 8642ecffaa..5b4d985114 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -71,7 +71,7 @@ "ngx-clipboard": "^16.0.0", "ngx-daterangepicker-material": "^6.0.4", "ngx-drag-drop": "^20.0.1", - "ngx-flowchart": "https://github.com/thingsboard/ngx-flowchart.git#release/4.0.0", + "ngx-flowchart": "file:../../ngx-flowchart/dist/ngx-flowchart", "ngx-hm-carousel": "^19.0.0", "ngx-markdown": "^20.1.0", "ngx-sharebuttons": "^17.0.0", diff --git a/ui-ngx/src/app/core/services/item-buffer.service.ts b/ui-ngx/src/app/core/services/item-buffer.service.ts index 7b7c828cf3..ba4b29cbcb 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -31,7 +31,7 @@ import { deepClone, isDefinedAndNotNull, isEqual } from '@core/utils'; import { UtilsService } from '@core/services/utils.service'; import { Observable, of, throwError } from 'rxjs'; import { map } from 'rxjs/operators'; -import { FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models'; +import { FcRuleNode, FcRuleNote, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models'; import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainImport } from '@shared/models/rule-chain.models'; import { Filter, FilterInfo, Filters, FiltersInfo, getFilterId } from '@shared/models/query/query.models'; @@ -72,6 +72,7 @@ export interface RuleNodeConnection { export interface RuleNodesReference { nodes: FcRuleNode[]; connections: RuleNodeConnection[]; + notes?: FcRuleNote[]; originX?: number; originY?: number; } @@ -312,10 +313,11 @@ export class ItemBufferService { return of(theDashboard); } - public copyRuleNodes(nodes: FcRuleNode[], connections: RuleNodeConnection[]) { + public copyRuleChainObjects(nodes: FcRuleNode[], connections: RuleNodeConnection[], notes: FcRuleNote[] = []) { const ruleNodes: RuleNodesReference = { nodes: [], - connections: [] + connections: [], + notes: [] }; let top = -1; let left = -1; @@ -364,6 +366,30 @@ export class ItemBufferService { right = Math.max(right, node.x + 170); } } + for (const origNote of notes) { + ruleNodes.notes.push({ + id: '', + x: origNote.x, + y: origNote.y, + width: origNote.width, + height: origNote.height, + content: origNote.content, + backgroundColor: origNote.backgroundColor, + applyDefaultMarkdownStyle: origNote.applyDefaultMarkdownStyle, + markdownCss: origNote.markdownCss + }); + if (top === -1) { + top = origNote.y; + left = origNote.x; + bottom = origNote.y + (origNote.height || 120); + right = origNote.x + (origNote.width || 200); + } else { + top = Math.min(top, origNote.y); + left = Math.min(left, origNote.x); + bottom = Math.max(bottom, origNote.y + (origNote.height || 120)); + right = Math.max(right, origNote.x + (origNote.width || 200)); + } + } ruleNodes.originX = left + (right - left) / 2; ruleNodes.originY = top + (bottom - top) / 2; connections.forEach(connection => { @@ -372,11 +398,11 @@ export class ItemBufferService { this.storeSet(RULE_NODES, ruleNodes); } - public hasRuleNodes(): boolean { + public hasRuleChainObjects(): boolean { return this.storeHas(RULE_NODES); } - public pasteRuleNodes(x: number, y: number): RuleNodesReference { + public pasteRuleChainObjects(x: number, y: number): RuleNodesReference { const ruleNodes: RuleNodesReference = this.storeGet(RULE_NODES); if (ruleNodes) { const deltaX = x - ruleNodes.originX; @@ -404,6 +430,10 @@ export class ItemBufferService { return null; } } + for (const note of (ruleNodes.notes || [])) { + note.x = Math.round(note.x + deltaX); + note.y = Math.round(note.y + deltaY); + } return ruleNodes; } return null; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html new file mode 100644 index 0000000000..5c98e69cd8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html @@ -0,0 +1,36 @@ + + +

rulechain.add-note

+ + +
+
+ + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss new file mode 100644 index 0000000000..fa37306f7b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.scss @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2026 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. + */ + +:host { + display: grid; + width: 650px; + height: 100%; + max-width: 100%; + max-height: 100vh; + grid-template-rows: min-content minmax(auto, 1fr) min-content; +} + +:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) { + .mat-mdc-dialog-content { + padding: 8px; + } +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.html new file mode 100644 index 0000000000..dcaea7550f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.html @@ -0,0 +1,46 @@ + +
+ + +
+ + + rule-node-config.advanced-settings + +
+
{{ 'rulechain.note-background-color' | translate }}
+ + +
+
+ + {{ 'rulechain.note-apply-default-markdown-style' | translate }} + +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.ts new file mode 100644 index 0000000000..37dceb3c03 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note.component.ts @@ -0,0 +1,57 @@ +/// +/// Copyright © 2016-2026 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, inject, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { FcRuleNote } from '@shared/models/rule-node.models'; + +@Component({ + selector: 'tb-rule-note', + templateUrl: './rule-note.component.html', + standalone: false +}) +export class RuleNoteComponent implements OnChanges { + + @Input() + note: FcRuleNote; + + private fb = inject(FormBuilder) + + noteForm = this.fb.group({ + content: [''], + backgroundColor: ['#FFF9C4'], + applyDefaultMarkdownStyle: [true], + markdownCss: [''] + }); + + ngOnChanges(changes: SimpleChanges): void { + if (changes.note) { + this.updatedForm(); + } + } + + private updatedForm(): void { + this.noteForm.setValue({ + content: this.note?.content || '', + backgroundColor: this.note?.backgroundColor || '#FFF9C4', + applyDefaultMarkdownStyle: this.note?.applyDefaultMarkdownStyle ?? true, + markdownCss: this.note?.markdownCss || '' + }) + setTimeout(() => { + this.noteForm.markAsPristine(); + }, 0); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html index 9d855647fa..849084fee9 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html @@ -84,7 +84,7 @@ @@ -158,6 +158,20 @@ [sourceRuleChainId]="editingRuleNodeSourceRuleChainId"> + + + + -
+
; connections: Array; + notes?: Array; } export interface RuleChainImport { diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 30e1648f7e..84f88ccbfa 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -18,7 +18,7 @@ import { BaseData } from '@shared/models/base-data'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { RuleNodeId } from '@shared/models/id/rule-node-id'; import { ComponentDescriptor } from '@shared/models/component-descriptor.models'; -import { FcEdge, FcNode } from 'ngx-flowchart'; +import { FcEdge, FcNode, FcNote } from 'ngx-flowchart'; import { Observable } from 'rxjs'; import { PageComponent } from '@shared/components/page.component'; import { AfterViewInit, DestroyRef, Directive, EventEmitter, inject, OnInit } from '@angular/core'; @@ -27,6 +27,7 @@ import { RuleChainType } from '@shared/models/rule-chain.models'; import { DebugRuleNodeEventBody } from '@shared/models/event.models'; import { EntityTestScriptResult, HasEntityDebugSettings } from '@shared/models/entity.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { strings } from '@angular-devkit/core'; export interface RuleNodeConfiguration { [key: string]: any; @@ -358,6 +359,13 @@ export interface FcRuleEdge extends FcEdge { labels?: string[]; } +export interface FcRuleNote extends FcNote { + content?: string; + backgroundColor?: string; + applyDefaultMarkdownStyle?: boolean; + markdownCss?: string; +} + export enum ScriptLanguage { JS = 'JS', TBEL = 'TBEL' diff --git a/ui-ngx/src/assets/help/en_US/rulechain/note_content.md b/ui-ngx/src/assets/help/en_US/rulechain/note_content.md new file mode 100644 index 0000000000..d21167be7f --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulechain/note_content.md @@ -0,0 +1,195 @@ +#### Markdown/HTML note content + +
+
+ +Notes support **Markdown** and **HTML** markup for rich text formatting directly on the rule chain canvas. + +
+
+ +#### Headings + +Use `#` to create headings. The number of `#` characters defines the level: + +```markdown +# Heading 1 +## Heading 2 +### Heading 3 +#### Heading 4 +{:copy-code} +``` + +
+
+ +#### Text formatting + +```markdown +**Bold text** +*Italic text* +~~Strikethrough~~ +**_Bold and italic_** +{:copy-code} +``` + +**Bold text** +*Italic text* +~~Strikethrough~~ +**_Bold and italic_** + +
+
+ +#### Lists + +Unordered list using `-` or `*`: + +```markdown +- Step 1: Receive telemetry +- Step 2: Filter by threshold + - Value > 100 → alarm branch + - Value ≤ 100 → success branch +- Step 3: Save to time series +{:copy-code} +``` + +- Step 1: Receive telemetry +- Step 2: Filter by threshold + - Value > 100 → alarm branch + - Value ≤ 100 → success branch +- Step 3: Save to time series + +Ordered list using numbers: + +```markdown +1. Validate message type +2. Enrich with device attributes +3. Route to appropriate handler +{:copy-code} +``` + +1. Validate message type +2. Enrich with device attributes +3. Route to appropriate handler + +
+
+ +#### Blockquotes + +Use `>` for notes, warnings, or highlighted information: + +```markdown +> **Note:** This node requires a valid device alias to be configured upstream. + +> **Warning:** Debug mode increases load — disable in production. +{:copy-code} +``` + +> **Note:** This node requires a valid device alias to be configured upstream. + +> **Warning:** Debug mode increases load — disable in production. + +
+
+ +#### Inline code and code blocks + +Use backticks for inline code and triple backticks for blocks: + +```markdown +The message type must be `POST_TELEMETRY_REQUEST`. +{:copy-code} +``` + +The message type must be `POST_TELEMETRY_REQUEST`. + +````markdown +```json +{ + "temperature": 42.5, + "humidity": 68 +} +``` +{:copy-code} +```` + +```json +{ + "temperature": 42.5, + "humidity": 68 +} +``` + +
+
+ +#### Tables + +```markdown +| Branch | Condition | Next node | +|----------|------------------|--------------------| +| True | temperature > 80 | Create Alarm | +| False | temperature ≤ 80 | Save Timeseries | +{:copy-code} +``` + +| Branch | Condition | Next node | +|----------|------------------|--------------------| +| True | temperature > 80 | Create Alarm | +| False | temperature ≤ 80 | Save Timeseries | + +
+
+ +#### Horizontal rule + +Use `---` to visually separate sections: + +```markdown +### Input + +Device telemetry message. + +--- + +### Output + +Enriched message with customer attributes. +{:copy-code} +``` + +
+
+ +#### HTML elements + +When **Apply default markdown style** is enabled, you can also use HTML tags: + +```html +🔴 Critical path
+🟢 Happy path +{:copy-code} +``` + +
+
+ +#### Tips for rule chain notes + +Use notes to annotate the canvas with context that helps your team: + +```markdown +## 🌡️ Temperature monitoring flow + +Processes telemetry from HVAC sensors. + +| Threshold | Severity | +|-----------|----------| +| > 85 °C | CRITICAL | +| > 70 °C | WARNING | + +> **Owner:** Platform team · **Updated:** 2026-02 +{:copy-code} +``` 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 5c527c9698..ff3a9cd639 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5165,7 +5165,17 @@ "unset-auto-assign-to-edge-title": "Are you sure you do not want to assign the edge rule chain '{{ruleChainName}}' to edge(s) on creation?", "unset-auto-assign-to-edge-text": "After the confirmation the edge rule chain will no longer be automatically assigned to edge(s) on creation.", "unassign-rulechain-title": "Are you sure you want to unassign the rulechain '{{ruleChainName}}'?", - "unassign-rulechains": "Unassign rulechains" + "unassign-rulechains": "Unassign rulechains", + "add-note": "Add note", + "paste-note": "Paste note", + "note": "Note", + "edit-note": "Edit note", + "note-content": "Markdown/HTML content", + "note-details": "Note details", + "note-empty": "Empty note", + "note-background-color": "Background color", + "note-apply-default-markdown-style": "Apply default markdown style", + "note-custom-css": "Note content CSS" }, "rulenode": { "rule-node-events": "Rule node events", diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 82fff6fbba..84e8586a4a 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -7989,9 +7989,8 @@ ngx-drag-drop@^20.0.1: dependencies: tslib "^2.3.0" -"ngx-flowchart@https://github.com/thingsboard/ngx-flowchart.git#release/4.0.0": +"ngx-flowchart@file:../../ngx-flowchart/dist/ngx-flowchart": version "4.0.0" - resolved "https://github.com/thingsboard/ngx-flowchart.git#735bc818ef218a169ac50e87edc6a093b3e97715" dependencies: tslib "^2.3.0" From db87b489f16c6b17df737a0656da5a0c60589bc9 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 27 Feb 2026 18:31:09 +0200 Subject: [PATCH 02/10] Implements support notes in RuleChain entity --- .../main/data/upgrade/basic/schema_update.sql | 6 ++ .../server/common/data/rule/RuleChain.java | 4 ++ .../common/data/rule/RuleChainMetaData.java | 5 ++ .../common/data/rule/RuleChainNote.java | 58 +++++++++++++++++++ .../server/dao/model/ModelConstants.java | 1 + .../server/dao/model/sql/RuleChainEntity.java | 5 ++ .../server/dao/rule/BaseRuleChainService.java | 7 +++ .../main/resources/sql/schema-entities.sql | 1 + 8 files changed, 87 insertions(+) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index a2dfeac358..1d4b4ec393 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,3 +14,9 @@ -- limitations under the License. -- + +-- RULE CHAIN NOTES START + +ALTER TABLE rule_chain ADD COLUMN IF NOT EXISTS notes text; + +-- RULE CHAIN NOTES END diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index 4d8bb35919..444dca319d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -61,6 +61,9 @@ public class RuleChain extends BaseDataWithAdditionalInfo implement private RuleChainId externalId; private Long version; + @JsonIgnore + private String notes; + @JsonIgnore private byte[] configurationBytes; @@ -82,6 +85,7 @@ public class RuleChain extends BaseDataWithAdditionalInfo implement this.setConfiguration(ruleChain.getConfiguration()); this.setExternalId(ruleChain.getExternalId()); this.version = ruleChain.getVersion(); + this.notes = ruleChain.getNotes(); } @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java index c9fdd1a4c6..9109df0f88 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.rule; +import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.HasVersion; @@ -48,6 +49,10 @@ public class RuleChainMetaData implements HasVersion { @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of JSON objects that represent connections between rule nodes and other rule chains.") private List ruleChainConnections; + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "List of sticky notes placed on the rule chain canvas") + private List notes; + public void addConnectionInfo(int fromIndex, int toIndex, String type) { NodeConnectionInfo connectionInfo = new NodeConnectionInfo(); connectionInfo.setFromIndex(fromIndex); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java new file mode 100644 index 0000000000..96715a6700 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2026 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. + */ +package org.thingsboard.server.common.data.rule; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * Represents a sticky note on the rule chain canvas. + * Notes are purely visual metadata and are stored as part of the rule chain. + */ +@Schema +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RuleChainNote { + + @Schema(description = "Unique identifier of the note on the canvas") + private String id; + + @Schema(description = "Horizontal position of the note on the canvas, in pixels") + private int x; + + @Schema(description = "Vertical position of the note on the canvas, in pixels") + private int y; + + @Schema(description = "Width of the note, in pixels") + private int width; + + @Schema(description = "Height of the note, in pixels") + private int height; + + @Schema(description = "Markdown or HTML content of the note") + private String content; + + @Schema(description = "Background color of the note in CSS hex format, e.g. '#FFF9C4'") + private String backgroundColor; + + @Schema(description = "Whether to apply the default markdown stylesheet to the note content") + private Boolean applyDefaultMarkdownStyle; + + @Schema(description = "Custom CSS styles applied to the note content") + private String markdownCss; + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 9f9e4a67e9..6eb14092ae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -420,6 +420,7 @@ public class ModelConstants { public static final String RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY = "first_rule_node_id"; public static final String RULE_CHAIN_ROOT_PROPERTY = "root"; public static final String RULE_CHAIN_CONFIGURATION_PROPERTY = "configuration"; + public static final String RULE_CHAIN_NOTES_PROPERTY = "notes"; /** * Rule node constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java index 5b83eb6f72..e408f41431 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainEntity.java @@ -69,6 +69,9 @@ public class RuleChainEntity extends BaseVersionedEntity { @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; + @Column(name = ModelConstants.RULE_CHAIN_NOTES_PROPERTY, columnDefinition = "TEXT") + private String notes; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) private UUID externalId; @@ -87,6 +90,7 @@ public class RuleChainEntity extends BaseVersionedEntity { this.debugMode = ruleChain.isDebugMode(); this.configuration = ruleChain.getConfiguration(); this.additionalInfo = ruleChain.getAdditionalInfo(); + this.notes = ruleChain.getNotes(); if (ruleChain.getExternalId() != null) { this.externalId = ruleChain.getExternalId().getId(); } @@ -107,6 +111,7 @@ public class RuleChainEntity extends BaseVersionedEntity { ruleChain.setDebugMode(debugMode); ruleChain.setConfiguration(configuration); ruleChain.setAdditionalInfo(additionalInfo); + ruleChain.setNotes(notes); if (externalId != null) { ruleChain.setExternalId(new RuleChainId(externalId)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 6b90b6fac6..3de45bcc15 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.rule; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FluentFuture; @@ -53,6 +54,7 @@ import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainNote; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; import org.thingsboard.server.common.data.rule.RuleNode; @@ -316,6 +318,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (!relations.isEmpty()) { relationService.saveRelations(tenantId, relations); } + ruleChain.setNotes(ruleChainMetaData.getNotes() != null && !ruleChainMetaData.getNotes().isEmpty() + ? JacksonUtil.toString(ruleChainMetaData.getNotes()) : null); ruleChain = ruleChainDao.save(tenantId, ruleChain); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain) .entityId(ruleChain.getId()).broadcastEvent(publishSaveEvent).build()); @@ -376,6 +380,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC Collections.sort(ruleChainMetaData.getConnections(), Comparator.comparingInt(NodeConnectionInfo::getFromIndex) .thenComparing(NodeConnectionInfo::getToIndex).thenComparing(NodeConnectionInfo::getType)); } + if (ruleChain.getNotes() != null) { + ruleChainMetaData.setNotes(JacksonUtil.fromString(ruleChain.getNotes(), new TypeReference>() {})); + } return ruleChainMetaData; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 3d9c9f17e6..c32ea29d10 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -169,6 +169,7 @@ CREATE TABLE IF NOT EXISTS rule_chain ( debug_mode boolean, tenant_id uuid, external_id uuid, + notes text, version BIGINT DEFAULT 1, CONSTRAINT rule_chain_external_id_unq_key UNIQUE (tenant_id, external_id) ); From 8b2f651644aece764af48419cb34bbc644c82134 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 27 Feb 2026 18:53:57 +0200 Subject: [PATCH 03/10] UI: Fixed context menu for edit note --- .../modules/home/pages/rulechain/rulechain-page.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 8391c874c8..f76fe391ee 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -1363,8 +1363,8 @@ export class RuleChainPageComponent extends PageComponent const contextInfo: RuleChainMenuContextInfo = { headerClass: 'tb-rulechain-header', icon: 'sticky_note_2', - title: note.content || '', - subtitle: this.translate.instant('rulechain.note'), + title: this.translate.instant('rulechain.note'), + subtitle: note.content.length > 24 ? note.content.substring(0, 24) + '…' : note.content, menuItems: [] }; if (!note.readonly) { From 664aeca9303936c7d607cf20aff47bd36c8cc5ef Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 27 Feb 2026 19:15:11 +0200 Subject: [PATCH 04/10] UI: Fixed rule chain note after review --- .../rulechain/add-note-dialog.component.html | 4 +-- ...t.html => rule-note-editor.component.html} | 0 ...onent.ts => rule-note-editor.component.ts} | 12 ++++----- .../rulechain/rulechain-page.component.html | 4 +-- .../rulechain/rulechain-page.component.ts | 27 +++++++++++-------- .../pages/rulechain/rulechain-page.module.ts | 4 +-- 6 files changed, 28 insertions(+), 23 deletions(-) rename ui-ngx/src/app/modules/home/pages/rulechain/{rule-note.component.html => rule-note-editor.component.html} (100%) rename ui-ngx/src/app/modules/home/pages/rulechain/{rule-note.component.ts => rule-note-editor.component.ts} (82%) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html index 5c98e69cd8..6359e32ff1 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-note-dialog.component.html @@ -23,8 +23,8 @@
- - + +
- + @if (!isImport) { + + + }