diff --git a/libs/gui-elements b/libs/gui-elements
index f2d447c0f9..3016f54232 160000
--- a/libs/gui-elements
+++ b/libs/gui-elements
@@ -1 +1 @@
-Subproject commit f2d447c0f9e3ce9b29f70075c47190b7cda328ad
+Subproject commit 3016f542321427dd250b117b8e8a04c308d48d5a
diff --git a/silk-core/src/main/resources/org/silkframework/LinkSpecificationLanguage.xsd b/silk-core/src/main/resources/org/silkframework/LinkSpecificationLanguage.xsd
index 4cee981cfb..7b8ec63dd5 100644
--- a/silk-core/src/main/resources/org/silkframework/LinkSpecificationLanguage.xsd
+++ b/silk-core/src/main/resources/org/silkframework/LinkSpecificationLanguage.xsd
@@ -190,9 +190,11 @@
-
-
-
+
+
+
+
+
diff --git a/silk-plugins/silk-serialization-json/src/main/scala/org/silkframework/serialization/json/JsonSerializers.scala b/silk-plugins/silk-serialization-json/src/main/scala/org/silkframework/serialization/json/JsonSerializers.scala
index 60229cd036..df430bfc27 100644
--- a/silk-plugins/silk-serialization-json/src/main/scala/org/silkframework/serialization/json/JsonSerializers.scala
+++ b/silk-plugins/silk-serialization-json/src/main/scala/org/silkframework/serialization/json/JsonSerializers.scala
@@ -30,6 +30,7 @@ import org.silkframework.workspace.annotation.{StickyNote, UiAnnotations}
import org.silkframework.workspace.{LoadedTask, TaskLoadingError}
import play.api.libs.json._
+import scala.collection.IndexedSeq
import scala.reflect.ClassTag
import scala.util.control.NonFatal
@@ -458,18 +459,46 @@ object JsonSerializers {
}
}
+ implicit object NodePositionJsonFormat extends JsonFormat[NodePosition] {
+
+ override def read(value: JsValue)(implicit readContext: ReadContext): NodePosition = {
+ value match {
+ case node: JsObject =>
+ NodePosition(
+ x = numberValue(node , "x").toInt,
+ y = numberValue(node, "y").toInt,
+ width = numberValueOption(node, "width").map(_.toInt),
+ height = numberValueOption(node, "height").map(_.toInt)
+ )
+ case JsArray(IndexedSeq(JsNumber(x), JsNumber(y))) =>
+ NodePosition(x.toInt, y.toInt)
+ case _ =>
+ throw JsonParseException("Invalid node position (must either be an array with two integers or an object): " + value)
+ }
+ }
+
+ override def write(value: NodePosition)(implicit writeContext: WriteContext[JsValue]): JsValue = {
+ Json.obj(
+ "x" -> value.x,
+ "y" -> value.y,
+ "width" -> value.width,
+ "height" -> value.height
+ )
+ }
+ }
+
/** Rule layout */
implicit object RuleLayoutJsonFormat extends JsonFormat[RuleLayout] {
final val NODE_POSITIONS = "nodePositions"
override def read(value: JsValue)(implicit readContext: ReadContext): RuleLayout = {
- val nodePositions = JsonHelpers.fromJsonValidated[Map[String, (Int, Int)]](mustBeDefined(value, NODE_POSITIONS))
+ val nodePositions = objectValue(value, NODE_POSITIONS).value.view.mapValues(NodePositionJsonFormat.read).toMap
RuleLayout(nodePositions)
}
override def write(value: RuleLayout)(implicit writeContext: WriteContext[JsValue]): JsValue = {
Json.obj(
- NODE_POSITIONS -> Json.toJson(value.nodePositions)
+ NODE_POSITIONS -> JsObject(value.nodePositions.view.mapValues(NodePositionJsonFormat.write).toSeq)
)
}
}
diff --git a/silk-plugins/silk-serialization-json/src/test/scala/JsonSerializersTest.scala b/silk-plugins/silk-serialization-json/src/test/scala/JsonSerializersTest.scala
index 6034647982..d3ca6d92c2 100644
--- a/silk-plugins/silk-serialization-json/src/test/scala/JsonSerializersTest.scala
+++ b/silk-plugins/silk-serialization-json/src/test/scala/JsonSerializersTest.scala
@@ -2,7 +2,7 @@
import org.silkframework.dataset._
import org.silkframework.entity.ValueType
import org.silkframework.rule.vocab._
-import org.silkframework.rule.{MappingTarget, RuleLayout}
+import org.silkframework.rule.{MappingTarget, NodePosition, RuleLayout}
import org.silkframework.runtime.activity.UserContext
import org.silkframework.runtime.plugin.PluginRegistry
import org.silkframework.runtime.serialization.{ReadContext, Serialization, TestReadContext, TestWriteContext, WriteContext}
@@ -63,9 +63,10 @@ class JsonSerializersTest extends AnyFlatSpec with Matchers {
"RuleLayout" should "be serializable to and from JSON" in {
val layout = RuleLayout(
Map(
- "nodeA" -> (1, 2),
- "nodeB" -> (3, 4),
- "nodeC" -> (5, 6)
+ "nodeA" -> NodePosition(1, 2),
+ "nodeB" -> NodePosition(3, 4, Some(10), None),
+ "nodeC" -> NodePosition(5, 6, None, Some(10)),
+ "nodeD" -> NodePosition(7, 8, Some(100), Some(200))
)
)
testSerialization(layout)
diff --git a/silk-rules/src/main/scala/org/silkframework/rule/RuleLayout.scala b/silk-rules/src/main/scala/org/silkframework/rule/RuleLayout.scala
index 2386f99872..d4814442ef 100644
--- a/silk-rules/src/main/scala/org/silkframework/rule/RuleLayout.scala
+++ b/silk-rules/src/main/scala/org/silkframework/rule/RuleLayout.scala
@@ -6,9 +6,9 @@ import scala.xml.Node
/** Rule layout data, i.e. how a linkage rule should be shown in a UI.
*
- * @param nodePositions The positions (x, y) of each rule node in the rule editor.
+ * @param nodePositions The position (x, y) and dimensions of each rule node in the rule editor.
*/
-case class RuleLayout(nodePositions: Map[String, (Int, Int)] = Map.empty)
+case class RuleLayout(nodePositions: Map[String, NodePosition] = Map.empty)
object RuleLayout {
private def textToInt(text: String): Int = Math.round(text.toDouble).toInt
@@ -19,7 +19,9 @@ object RuleLayout {
val nodeId = (nodePos \ "@id").text
val x = textToInt((nodePos \ "@x").text)
val y = textToInt((nodePos \ "@y").text)
- (nodeId, (x, y))
+ val width = (nodePos \ "@width").headOption.map(n => textToInt(n.text))
+ val height = (nodePos \ "@height").headOption.map(n => textToInt(n.text))
+ (nodeId, NodePosition(x, y, width, height))
})
RuleLayout(positions.toMap)
}
@@ -27,11 +29,21 @@ object RuleLayout {
override def write(value: RuleLayout)(implicit writeContext: WriteContext[Node]): Node = {
- {value.nodePositions.map { case (nodeId, (x, y)) =>
-
+ { value.nodePositions.map { case (nodeId, pos) =>
+
}}
}
}
-}
\ No newline at end of file
+}
+
+/**
+ * Holds the position and the width of an operator.
+ *
+ * @param x The x coordinate
+ * @param y The y coordinate
+ * @param width An optional used-defined width. If not provided, the width should be determined by the UI.
+ * @param height An optional used-defined height. If not provided, the width should be determined by the UI.
+ */
+case class NodePosition(x: Int, y: Int, width: Option[Int] = None, height: Option[Int] = None)
\ No newline at end of file
diff --git a/silk-rules/src/test/scala/org/silkframework/rule/RuleLayoutTest.scala b/silk-rules/src/test/scala/org/silkframework/rule/RuleLayoutTest.scala
index acab76d87d..cc97e7b2dc 100644
--- a/silk-rules/src/test/scala/org/silkframework/rule/RuleLayoutTest.scala
+++ b/silk-rules/src/test/scala/org/silkframework/rule/RuleLayoutTest.scala
@@ -1,20 +1,22 @@
-package org.silkframework.rule
-
+package org.silkframework.rule
+
+
import org.silkframework.util.XmlSerializationHelperTrait
import org.scalatest.flatspec.AnyFlatSpec
-import org.scalatest.matchers.must.Matchers
-
-class RuleLayoutTest extends AnyFlatSpec with Matchers with XmlSerializationHelperTrait {
- behavior of "Rule layout"
-
- it should "serialize and deserialize" in {
- val layout = RuleLayout(
- Map(
- "nodeA" -> (1, 2),
- "nodeB" -> (3, 4),
- "nodeC" -> (5, 6)
- )
- )
- testRoundTripSerialization(layout)
- }
-}
+import org.scalatest.matchers.must.Matchers
+
+class RuleLayoutTest extends AnyFlatSpec with Matchers with XmlSerializationHelperTrait {
+ behavior of "Rule layout"
+
+ it should "serialize and deserialize" in {
+ val layout = RuleLayout(
+ Map(
+ "nodeA" -> NodePosition(1, 2),
+ "nodeB" -> NodePosition(3, 4, Some(10), None),
+ "nodeC" -> NodePosition(5, 6, None, Some(10)),
+ "nodeD" -> NodePosition(7, 8, Some(100), Some(200))
+ )
+ )
+ testRoundTripSerialization(layout)
+ }
+}
diff --git a/silk-workspace/src/test/scala/org/silkframework/workspace/WorkspaceProviderTestTrait.scala b/silk-workspace/src/test/scala/org/silkframework/workspace/WorkspaceProviderTestTrait.scala
index ccde608ac3..47b7a497ab 100644
--- a/silk-workspace/src/test/scala/org/silkframework/workspace/WorkspaceProviderTestTrait.scala
+++ b/silk-workspace/src/test/scala/org/silkframework/workspace/WorkspaceProviderTestTrait.scala
@@ -84,7 +84,7 @@ trait WorkspaceProviderTestTrait extends AnyFlatSpec with Matchers with MockitoS
inverseLinkType = Some("http://www.w3.org/2002/07/owl#sameAsInv"),
excludeSelfReferences = true,
layout = RuleLayout(
- nodePositions = Map("compareNames" -> (1, 2))
+ nodePositions = Map("compareNames" -> NodePosition(1, 2))
),
uiAnnotations = UiAnnotations(
stickyNotes = Seq(StickyNote("compareNames", "content", "#fff", (0, 0), (1, 1)))
@@ -147,9 +147,9 @@ trait WorkspaceProviderTestTrait extends AnyFlatSpec with Matchers with MockitoS
target = Some(MappingTarget(Uri("urn:complex:target"))),
layout = RuleLayout(
nodePositions = Map(
- "lower" -> (0, 1),
- "concat" -> (3, 4),
- "path" -> (5, 6)
+ "lower" -> NodePosition(0, 1),
+ "concat" -> NodePosition(3, 4, Some(250)),
+ "path" -> NodePosition(5, 6, Some(250), Some(300))
)
),
uiAnnotations = UiAnnotations(
diff --git a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/MappingEditorModal.tsx b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/MappingEditorModal.tsx
index 72a6aa976f..2a91910340 100644
--- a/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/MappingEditorModal.tsx
+++ b/workspace/src/app/views/pages/MappingEditor/HierarchicalMapping/MappingEditorModal.tsx
@@ -92,6 +92,9 @@ const MappingEditorModal = ({
size="fullscreen"
preventSimpleClosing={unsavedChanges}
onClose={onClose}
+ wrapperDivProps={{
+ onMouseUp: () => {},
+ }}
headerOptions={
{
) => Map | Promise