diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index a2dfeac358..f22eea3712 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,3 +14,8 @@ -- limitations under the License. -- +-- RULE CHAIN NOTES MIGRATION START + +ALTER TABLE rule_chain ADD COLUMN IF NOT EXISTS notes varchar(10000000); + +-- RULE CHAIN NOTES MIGRATION END diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainDetails.java new file mode 100644 index 0000000000..c09cc8aef3 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainDetails.java @@ -0,0 +1,46 @@ +/** + * 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; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class RuleChainDetails extends RuleChain { + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @Schema(description = "List of sticky notes placed on the rule chain canvas") + private List notes; + + public RuleChainDetails() { + super(); + } + + public RuleChainDetails(RuleChain ruleChain) { + super(ruleChain); + } + + public RuleChainDetails(RuleChainDetails ruleChainDetails) { + super(ruleChainDetails); + this.notes = ruleChainDetails.getNotes(); + } + +} 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..91e2926ac8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainNote.java @@ -0,0 +1,64 @@ +/** + * 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 = "Border color of the note in CSS hex format, e.g. '#E6C800'") + private String borderColor; + + @Schema(description = "Border width of the note in pixels") + private Integer borderWidth; + + @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/AbstractRuleChainEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractRuleChainEntity.java new file mode 100644 index 0000000000..71b729ae76 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractRuleChainEntity.java @@ -0,0 +1,127 @@ +/** + * 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.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.MappedSuperclass; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.BaseVersionedEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonConverter; + +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@MappedSuperclass +public abstract class AbstractRuleChainEntity extends BaseVersionedEntity { + + @Column(name = ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ModelConstants.RULE_CHAIN_NAME_PROPERTY) + private String name; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.RULE_CHAIN_TYPE_PROPERTY) + private RuleChainType type; + + @Column(name = ModelConstants.RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY) + private UUID firstRuleNodeId; + + @Column(name = ModelConstants.RULE_CHAIN_ROOT_PROPERTY) + private boolean root; + + @Column(name = ModelConstants.DEBUG_MODE) + private boolean debugMode; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.RULE_CHAIN_CONFIGURATION_PROPERTY) + private JsonNode configuration; + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) + private JsonNode additionalInfo; + + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) + private UUID externalId; + + public AbstractRuleChainEntity() { + super(); + } + + public AbstractRuleChainEntity(T ruleChain) { + super(ruleChain); + this.tenantId = DaoUtil.getId(ruleChain.getTenantId()); + this.name = ruleChain.getName(); + this.type = ruleChain.getType(); + if (ruleChain.getFirstRuleNodeId() != null) { + this.firstRuleNodeId = ruleChain.getFirstRuleNodeId().getId(); + } + this.root = ruleChain.isRoot(); + this.debugMode = ruleChain.isDebugMode(); + this.configuration = ruleChain.getConfiguration(); + this.additionalInfo = ruleChain.getAdditionalInfo(); + if (ruleChain.getExternalId() != null) { + this.externalId = ruleChain.getExternalId().getId(); + } + } + + public AbstractRuleChainEntity(AbstractRuleChainEntity ruleChainEntity) { + super(ruleChainEntity); + this.tenantId = ruleChainEntity.getTenantId(); + this.name = ruleChainEntity.getName(); + this.type = ruleChainEntity.getType(); + this.firstRuleNodeId = ruleChainEntity.getFirstRuleNodeId(); + this.root = ruleChainEntity.isRoot(); + this.debugMode = ruleChainEntity.isDebugMode(); + this.configuration = ruleChainEntity.getConfiguration(); + this.additionalInfo = ruleChainEntity.getAdditionalInfo(); + this.externalId = ruleChainEntity.getExternalId(); + } + + protected RuleChain toRuleChain() { + RuleChain ruleChain = new RuleChain(new RuleChainId(this.getUuid())); + ruleChain.setCreatedTime(createdTime); + ruleChain.setVersion(version); + ruleChain.setTenantId(TenantId.fromUUID(tenantId)); + ruleChain.setName(name); + ruleChain.setType(type); + if (firstRuleNodeId != null) { + ruleChain.setFirstRuleNodeId(new RuleNodeId(firstRuleNodeId)); + } + ruleChain.setRoot(root); + ruleChain.setDebugMode(debugMode); + ruleChain.setConfiguration(configuration); + ruleChain.setAdditionalInfo(additionalInfo); + if (externalId != null) { + ruleChain.setExternalId(new RuleChainId(externalId)); + } + return ruleChain; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainDetailsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainDetailsEntity.java new file mode 100644 index 0000000000..732687016e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleChainDetailsEntity.java @@ -0,0 +1,65 @@ +/** + * 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.dao.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainDetails; +import org.thingsboard.server.common.data.rule.RuleChainNote; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonConverter; + +import java.util.Arrays; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = ModelConstants.RULE_CHAIN_TABLE_NAME) +public class RuleChainDetailsEntity extends AbstractRuleChainEntity { + + @Convert(converter = JsonConverter.class) + @Column(name = ModelConstants.RULE_CHAIN_NOTES_PROPERTY) + private JsonNode notes; + + public RuleChainDetailsEntity() { + super(); + } + + public RuleChainDetailsEntity(RuleChainDetails ruleChainDetails) { + super(ruleChainDetails); + if (ruleChainDetails.getNotes() != null) { + this.notes = JacksonUtil.valueToTree(ruleChainDetails.getNotes()); + } + } + + @Override + public RuleChainDetails toData() { + RuleChain ruleChain = super.toRuleChain(); + RuleChainDetails details = new RuleChainDetails(ruleChain); + if (notes != null && notes.isArray()) { + details.setNotes(Arrays.asList(JacksonUtil.treeToValue(notes, RuleChainNote[].class))); + } + return details; + } + +} 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..ca6651a965 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 @@ -15,102 +15,30 @@ */ package org.thingsboard.server.dao.model.sql; -import com.fasterxml.jackson.databind.JsonNode; -import jakarta.persistence.Column; -import jakarta.persistence.Convert; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; -import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.id.RuleNodeId; -import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; -import org.thingsboard.server.common.data.rule.RuleChainType; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.model.BaseVersionedEntity; import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.util.mapping.JsonConverter; - -import java.util.UUID; @Data @EqualsAndHashCode(callSuper = true) @Entity @Table(name = ModelConstants.RULE_CHAIN_TABLE_NAME) -public class RuleChainEntity extends BaseVersionedEntity { - - @Column(name = ModelConstants.RULE_CHAIN_TENANT_ID_PROPERTY) - private UUID tenantId; - - @Column(name = ModelConstants.RULE_CHAIN_NAME_PROPERTY) - private String name; - - @Enumerated(EnumType.STRING) - @Column(name = ModelConstants.RULE_CHAIN_TYPE_PROPERTY) - private RuleChainType type; - - @Column(name = ModelConstants.RULE_CHAIN_FIRST_RULE_NODE_ID_PROPERTY) - private UUID firstRuleNodeId; - - @Column(name = ModelConstants.RULE_CHAIN_ROOT_PROPERTY) - private boolean root; - - @Column(name = ModelConstants.DEBUG_MODE) - private boolean debugMode; - - @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.RULE_CHAIN_CONFIGURATION_PROPERTY) - private JsonNode configuration; - - @Convert(converter = JsonConverter.class) - @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) - private JsonNode additionalInfo; - - @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) - private UUID externalId; +public class RuleChainEntity extends AbstractRuleChainEntity { public RuleChainEntity() { + super(); } public RuleChainEntity(RuleChain ruleChain) { super(ruleChain); - this.tenantId = DaoUtil.getId(ruleChain.getTenantId()); - this.name = ruleChain.getName(); - this.type = ruleChain.getType(); - if (ruleChain.getFirstRuleNodeId() != null) { - this.firstRuleNodeId = ruleChain.getFirstRuleNodeId().getId(); - } - this.root = ruleChain.isRoot(); - this.debugMode = ruleChain.isDebugMode(); - this.configuration = ruleChain.getConfiguration(); - this.additionalInfo = ruleChain.getAdditionalInfo(); - if (ruleChain.getExternalId() != null) { - this.externalId = ruleChain.getExternalId().getId(); - } } @Override public RuleChain toData() { - RuleChain ruleChain = new RuleChain(new RuleChainId(this.getUuid())); - ruleChain.setCreatedTime(createdTime); - ruleChain.setVersion(version); - ruleChain.setTenantId(TenantId.fromUUID(tenantId)); - ruleChain.setName(name); - ruleChain.setType(type); - if (firstRuleNodeId != null) { - ruleChain.setFirstRuleNodeId(new RuleNodeId(firstRuleNodeId)); - } - ruleChain.setRoot(root); - ruleChain.setDebugMode(debugMode); - ruleChain.setConfiguration(configuration); - ruleChain.setAdditionalInfo(additionalInfo); - if (externalId != null) { - ruleChain.setExternalId(new RuleChainId(externalId)); - } - return ruleChain; + return super.toRuleChain(); } } 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..50a2d148d7 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 @@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainData; +import org.thingsboard.server.common.data.rule.RuleChainDetails; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -316,9 +317,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (!relations.isEmpty()) { relationService.saveRelations(tenantId, relations); } - ruleChain = ruleChainDao.save(tenantId, ruleChain); - eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChain) - .entityId(ruleChain.getId()).broadcastEvent(publishSaveEvent).build()); + RuleChainDetails ruleChainDetails = new RuleChainDetails(ruleChain); + ruleChainDetails.setNotes(ruleChainMetaData.getNotes()); + ruleChainDetails = ruleChainDao.saveDetails(ruleChainDetails); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entity(ruleChainDetails) + .entityId(ruleChainDetails.getId()).broadcastEvent(publishSaveEvent).build()); return RuleChainUpdateResult.successful(updatedRuleNodes); } @@ -341,13 +344,13 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override public RuleChainMetaData loadRuleChainMetaData(TenantId tenantId, RuleChainId ruleChainId) { Validator.validateId(ruleChainId, "Incorrect rule chain id."); - RuleChain ruleChain = findRuleChainById(tenantId, ruleChainId); - if (ruleChain == null) { + RuleChainDetails ruleChainDetails = ruleChainDao.findDetailsById(ruleChainId.getId()); + if (ruleChainDetails == null) { return null; } RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); ruleChainMetaData.setRuleChainId(ruleChainId); - ruleChainMetaData.setVersion(ruleChain.getVersion()); + ruleChainMetaData.setVersion(ruleChainDetails.getVersion()); List ruleNodes = getRuleChainNodes(tenantId, ruleChainId); Collections.sort(ruleNodes, Comparator.comparingLong(RuleNode::getCreatedTime).thenComparing(RuleNode::getId, Comparator.comparing(RuleNodeId::getId))); Map ruleNodeIndexMap = new HashMap<>(); @@ -355,8 +358,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC ruleNodeIndexMap.put(node.getId(), ruleNodes.indexOf(node)); } ruleChainMetaData.setNodes(ruleNodes); - if (ruleChain.getFirstRuleNodeId() != null) { - ruleChainMetaData.setFirstNodeIndex(ruleNodeIndexMap.get(ruleChain.getFirstRuleNodeId())); + if (ruleChainDetails.getFirstRuleNodeId() != null) { + ruleChainMetaData.setFirstNodeIndex(ruleNodeIndexMap.get(ruleChainDetails.getFirstRuleNodeId())); } for (RuleNode node : ruleNodes) { int fromIndex = ruleNodeIndexMap.get(node.getId()); @@ -376,6 +379,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC Collections.sort(ruleChainMetaData.getConnections(), Comparator.comparingInt(NodeConnectionInfo::getFromIndex) .thenComparing(NodeConnectionInfo::getToIndex).thenComparing(NodeConnectionInfo::getType)); } + ruleChainMetaData.setNotes(ruleChainDetails.getNotes()); return ruleChainMetaData; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index b88ed2294b..bec8ef0144 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainDetails; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; @@ -85,4 +86,8 @@ public interface RuleChainDao extends Dao, TenantEntityDao List findRuleChainsByTenantIdAndIds(UUID tenantId, List ruleChainIds); + RuleChainDetails findDetailsById(UUID id); + + RuleChainDetails saveDetails(RuleChainDetails ruleChainDetails); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index a72b053a98..19cf95e5bc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -29,8 +29,10 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainDetails; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.RuleChainDetailsEntity; import org.thingsboard.server.dao.model.sql.RuleChainEntity; import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -49,6 +51,9 @@ public class JpaRuleChainDao extends JpaAbstractDao @Autowired private RuleChainRepository ruleChainRepository; + @Autowired + private RuleChainDetailsRepository ruleChainDetailsRepository; + @Override protected Class getEntityClass() { return RuleChainEntity.class; @@ -163,6 +168,16 @@ public class JpaRuleChainDao extends JpaAbstractDao return ruleChainRepository.findNextBatch(id, Limit.of(batchSize)); } + @Override + public RuleChainDetails findDetailsById(UUID id) { + return DaoUtil.getData(ruleChainDetailsRepository.findById(id)); + } + + @Override + public RuleChainDetails saveDetails(RuleChainDetails ruleChainDetails) { + return DaoUtil.getData(ruleChainDetailsRepository.save(new RuleChainDetailsEntity(ruleChainDetails))); + } + @Override public EntityType getEntityType() { return EntityType.RULE_CHAIN; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainDetailsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainDetailsRepository.java new file mode 100644 index 0000000000..df54720984 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainDetailsRepository.java @@ -0,0 +1,25 @@ +/** + * 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.dao.sql.rule; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.thingsboard.server.dao.model.sql.RuleChainDetailsEntity; + +import java.util.UUID; + +public interface RuleChainDetailsRepository extends JpaRepository { + +} diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 3d9c9f17e6..3e7ea93ead 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -162,6 +162,7 @@ CREATE TABLE IF NOT EXISTS rule_chain ( created_time bigint NOT NULL, additional_info varchar, configuration varchar(10000000), + notes varchar(10000000), name varchar(255), type varchar(255), first_rule_node_id uuid, diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 8642ecffaa..6e1ea4bf33 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": "https://github.com/thingsboard/ngx-flowchart.git#release/4.1.0", "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..1dc7146883 100644 --- a/ui-ngx/src/app/core/services/item-buffer.service.ts +++ b/ui-ngx/src/app/core/services/item-buffer.service.ts @@ -16,7 +16,13 @@ import { Injectable } from '@angular/core'; import { BreakpointId, Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; -import { AliasesInfo, EntityAlias, EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; +import { + AliasesInfo, + EntityAlias, + EntityAliases, + EntityAliasInfo, + getEntityAliasId +} from '@shared/models/alias.models'; import { Datasource, DatasourceType, @@ -31,7 +37,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,8 +78,11 @@ export interface RuleNodeConnection { export interface RuleNodesReference { nodes: FcRuleNode[]; connections: RuleNodeConnection[]; + notes?: FcRuleNote[]; originX?: number; originY?: number; + minX?: number; + minY?: number; } @Injectable({ @@ -312,10 +321,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,23 +374,51 @@ 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, + borderWidth: origNote.borderWidth, + borderColor: origNote.borderColor, + 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; + ruleNodes.minX = left; + ruleNodes.minY = top; connections.forEach(connection => { ruleNodes.connections.push(connection); }); 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; - const deltaY = y - ruleNodes.originY; + const deltaX = isDefinedAndNotNull(ruleNodes.minX) ? Math.max(x - ruleNodes.originX, -ruleNodes.minX) : x - ruleNodes.originX; + const deltaY = isDefinedAndNotNull(ruleNodes.minY) ? Math.max(y - ruleNodes.originY, -ruleNodes.minY) : y - ruleNodes.originY; for (const node of ruleNodes.nodes) { const component = this.ruleChainService.getRuleNodeComponentByClazz(node.ruleChainType, node.componentClazz); if (component) { @@ -404,6 +442,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..33aa61d0e1 --- /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-editor.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.html new file mode 100644 index 0000000000..015c2a38d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.html @@ -0,0 +1,59 @@ + +
+ + +
+
{{ 'rulechain.note-background-color' | translate }}
+ + +
+
+ + + rule-node-config.advanced-settings + +
+
{{ 'rulechain.note-border' | translate }}
+
+ + + px + + + +
+
+
+ + {{ 'rulechain.note-apply-default-markdown-style' | translate }} + +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.scss new file mode 100644 index 0000000000..6d89be41ad --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.scss @@ -0,0 +1,24 @@ +/** + * 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 ::ng-deep { + tb-markdown-editor { + .markdown-content.tb-edit-mode { + &:not(.tb-fullscreen) { + padding-bottom: 0; + } + } + } +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.ts new file mode 100644 index 0000000000..a5a6bb1622 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-note-editor.component.ts @@ -0,0 +1,106 @@ +/// +/// 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, DestroyRef, inject, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatExpansionPanel } from '@angular/material/expansion'; +import { FormBuilder, Validators } from '@angular/forms'; +import { + FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE as DEFAULT_APPLY_MARKDOWN_STYLE, + FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR as DEFAULT_BACKGROUND_COLOR, + FC_RULE_NOTE_DEFAULT_BORDER_WIDTH as DEFAULT_BORDER_WIDTH, + FcRuleNote +} from '@shared/models/rule-node.models'; +import { isNotEmptyStr } from '@core/utils'; +import tinycolor from 'tinycolor2'; + +@Component({ + selector: 'tb-rule-note-editor', + templateUrl: './rule-note-editor.component.html', + styleUrls: ['./rule-note-editor.component.scss'], + standalone: false +}) +export class RuleNoteEditorComponent implements OnChanges, OnInit { + + @Input() + note: FcRuleNote; + + @ViewChild('advancedPanel') advancedPanel: MatExpansionPanel; + + private fb = inject(FormBuilder); + private destroyRef = inject(DestroyRef); + + noteForm = this.fb.group({ + content: ['', Validators.maxLength(65536)], + backgroundColor: [DEFAULT_BACKGROUND_COLOR], + borderColor: [tinycolor(DEFAULT_BACKGROUND_COLOR).darken(20).toString()], + borderWidth: [DEFAULT_BORDER_WIDTH, [Validators.min(0)]], + applyDefaultMarkdownStyle: [DEFAULT_APPLY_MARKDOWN_STYLE], + markdownCss: ['', Validators.maxLength(65536)] + }); + + ngOnInit(): void { + this.noteForm.get('backgroundColor').valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(color => { + const borderControl = this.noteForm.get('borderColor'); + if (borderControl.pristine) { + borderControl.setValue(this.borderColorFrom(color), { emitEvent: false }); + } + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.note) { + this.updatedForm(); + } + } + + private borderColorFrom(bg: string): string { + const color = tinycolor(bg || DEFAULT_BACKGROUND_COLOR); + return color.isDark() ? color.lighten(20).toString() : color.darken(20).toString(); + } + + private updatedForm(): void { + const bg = this.note?.backgroundColor || DEFAULT_BACKGROUND_COLOR; + this.noteForm.setValue({ + content: this.note?.content || '', + backgroundColor: bg, + borderColor: this.note?.borderColor || this.borderColorFrom(bg), + borderWidth: this.note?.borderWidth ?? DEFAULT_BORDER_WIDTH, + applyDefaultMarkdownStyle: this.note?.applyDefaultMarkdownStyle ?? DEFAULT_APPLY_MARKDOWN_STYLE, + markdownCss: this.note?.markdownCss || '' + }); + const shouldExpand = this.hasNonDefaultAdvancedSettings(bg); + setTimeout(() => { + if (shouldExpand) { + this.advancedPanel?.open(); + } else { + this.advancedPanel?.close(); + } + this.noteForm.markAsPristine(); + }, 0); + } + + private hasNonDefaultAdvancedSettings(bg: string): boolean { + const note = this.note; + if (!note) return false; + return (note.borderColor != null && note.borderColor !== this.borderColorFrom(bg)) + || (note.borderWidth != null && note.borderWidth !== DEFAULT_BORDER_WIDTH) + || note.applyDefaultMarkdownStyle === false + || isNotEmptyStr(note.markdownCss); + } +} 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..0a9a085b93 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"> + + + + - + @if (!isImport) { + + + } -
+
; 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..1789e5c5d8 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,19 @@ export interface FcRuleEdge extends FcEdge { labels?: string[]; } +export const FC_RULE_NOTE_DEFAULT_BACKGROUND_COLOR = '#FFF9C4'; +export const FC_RULE_NOTE_DEFAULT_BORDER_WIDTH = 1; +export const FC_RULE_NOTE_DEFAULT_APPLY_MARKDOWN_STYLE = true; + +export interface FcRuleNote extends FcNote { + content?: string; + backgroundColor?: string; + borderColor?: string; + borderWidth?: number; + 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..e63ced9895 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-background-color": "Background color", + "note-border": "Border", + "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..f73daa94a6 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -7989,9 +7989,9 @@ ngx-drag-drop@^20.0.1: dependencies: tslib "^2.3.0" -"ngx-flowchart@https://github.com/thingsboard/ngx-flowchart.git#release/4.0.0": - version "4.0.0" - resolved "https://github.com/thingsboard/ngx-flowchart.git#735bc818ef218a169ac50e87edc6a093b3e97715" +"ngx-flowchart@https://github.com/thingsboard/ngx-flowchart.git#release/4.1.0": + version "4.1.0" + resolved "https://github.com/thingsboard/ngx-flowchart.git#a3dee1761ecaa96c7d0ca22b1c6538b9e27103af" dependencies: tslib "^2.3.0"