This is an automated email from the ASF dual-hosted git repository.

msyavuz pushed a commit to branch msyavuz/feat/canvas
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 146af62670d38e739e8261f63fa67e92a32cd721
Author: Mehmet Salih Yavuz <[email protected]>
AuthorDate: Thu Oct 1 17:10:52 2026 +0300

    feat(canvas): allow a widget to be placed more than once
    
    Placement, scopes, filter values and refresh exemptions are all keyed by
    node, so two nodes can reference the same widget without sharing any
    canvas state.
---
 superset/canvas/definition/validation.py   | 13 ++++---------
 tests/unit_tests/canvas/validation_test.py | 11 +++++++----
 2 files changed, 11 insertions(+), 13 deletions(-)

diff --git a/superset/canvas/definition/validation.py 
b/superset/canvas/definition/validation.py
index d04a5440e71..b485e27547d 100644
--- a/superset/canvas/definition/validation.py
+++ b/superset/canvas/definition/validation.py
@@ -17,9 +17,8 @@
 """
 Whole-canvas validation and normalization.
 
-Checks the envelope, tree integrity, that every widget is placed only once, the
-nesting rules of widget types, each child's layout under its parent, and
-resolves grid collisions.
+Checks the envelope, tree integrity, the nesting rules of widget types and each
+child's layout under its parent, and resolves grid collisions.
 
 A node whose widget no longer exists, or whose container type is no longer
 registered (e.g. its extension was removed), is kept as an unresolved
@@ -170,14 +169,10 @@ class _Validator:
     def _check_widgets(self, doc: dict[str, Any]) -> None:
         nodes = doc["nodes"]
         types = self.resolver.widget_types({node["widget"] for node in 
nodes.values()})
-        placed: dict[str, str] = {}
+        # A widget may be placed more than once; everything the canvas tracks
+        # (placement, scopes, values) is keyed by node.
         for node_id, node in nodes.items():
             widget_id = node["widget"]
-            path = pointer("nodes", node_id, "widget")
-            if widget_id in placed:
-                self.fail(path, f"widget {widget_id!r} is already placed")
-                continue
-            placed[widget_id] = node_id
             widget_type = types.get(widget_id)
             if widget_type is None:
                 self.unresolved.add(node_id)
diff --git a/tests/unit_tests/canvas/validation_test.py 
b/tests/unit_tests/canvas/validation_test.py
index 681b0e5ef60..c42d4c9da8e 100644
--- a/tests/unit_tests/canvas/validation_test.py
+++ b/tests/unit_tests/canvas/validation_test.py
@@ -84,10 +84,13 @@ def test_missing_widgets_stay_as_placeholders() -> None:
     assert result["nodes"]["b"]["layout"] == {"anything": "kept"}
 
 
-def test_a_widget_is_placed_once() -> None:
-    assert issues(canvas({"a": node("chart-1"), "b": node("chart-1")})) == [
-        ("/nodes/b/widget", "widget 'chart-1' is already placed")
-    ]
+def test_a_widget_can_be_placed_more_than_once() -> None:
+    raw = canvas({"a": node("chart-1", layout={"colSpan": 8}), "b": 
node("chart-1")})
+
+    result = normalize(raw)
+
+    assert result["nodes"]["a"]["widget"] == result["nodes"]["b"]["widget"]
+    assert result["nodes"]["a"]["layout"] == {"colSpan": 8}
 
 
 def test_unknown_child_and_orphan() -> None:

Reply via email to