Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ an error boundary keeps a malformed graph from blanking the canvas.
`nodes` and `edges` once
([#72](https://github.com/panel-extensions/panel-reactflow/pull/72)).

- **Serialization error when updating `node_types`** — assigning
`NodeType` or `EdgeType` descriptors with a `schema` class after
construction synced them to the browser before they were converted to
JSON descriptors, so the server failed with `can't serialize
ParameterizedMetaclass`. They are now converted before syncing, and
updating `node_types` and `edge_types` together no longer raises a
`TypeError`
([#76](https://github.com/panel-extensions/panel-reactflow/pull/76)).

## Version 0.4.1

A small enhancement release adding viewport zoom controls.
Expand Down
18 changes: 11 additions & 7 deletions src/panel_reactflow/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2277,8 +2277,12 @@ def _process_param_change(self, params):
params["nodes"] = nodes
if "edges" in params:
params["edges"] = [self._edge_payload(edge) for edge in params["edges"]]
# node_types / edge_types are now JSON-serializable descriptors
# and intentionally synced to the frontend.
# Panel syncs a new value before `_normalize_specs` replaces it, so
# schema classes must be coerced here to stay out of the payload.
if "node_types" in params:
params["node_types"] = _coerce_spec_map(params["node_types"])
if "edge_types" in params:
params["edge_types"] = _coerce_spec_map(params["edge_types"], edge=True)
# Pop Python-only editor registries and internal state.
params.pop("node_editors", None)
params.pop("edge_editors", None)
Expand Down Expand Up @@ -3642,11 +3646,11 @@ def _update_selection_from_graph(self, *_: param.parameterized.Event) -> None:
{"type": "selection_changed", "nodes": selection["nodes"], "edges": selection["edges"]},
)

def _normalize_specs(self, event: param.parameterized.Event) -> None:
is_edge = event.name == "edge_types"
normalized = _coerce_spec_map(event.new, edge=is_edge)
if normalized != event.new:
setattr(self, event.name, normalized)
def _normalize_specs(self, *events: param.parameterized.Event) -> None:
for event in events:
normalized = _coerce_spec_map(event.new, edge=event.name == "edge_types")
if normalized != event.new:
setattr(self, event.name, normalized)

def _normalize_nodes(self, event: param.parameterized.Event) -> None:
"""Normalize nodes list by converting NodeSpec objects to dicts."""
Expand Down
35 changes: 34 additions & 1 deletion tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Tests for ReactFlow model creation."""

import param
from panel.pane import Markdown
from panel.viewable import Viewer

from panel_reactflow import Edge, Node, ReactFlow
from panel_reactflow import Edge, EdgeType, Node, NodeType, ReactFlow


def test_reactflow_add_node_with_arbitrary_object(document, comm) -> None:
Expand Down Expand Up @@ -121,3 +122,35 @@ def editor(self, data, schema, *, id, type, on_patch):
by_id = {node["id"]: node for node in model.data.nodes}
assert by_id["n1"]["data"]["view_idx"] == 0
assert by_id["n2"]["data"].get("view_idx") is None


class _Config(param.Parameterized):
x = param.Integer(default=1)


def test_updated_node_types_reach_model_as_descriptors(document, comm) -> None:
flow = ReactFlow()
model = flow.get_root(document, comm=comm)
synced = []
model.data.on_change("node_types", lambda attr, old, new: synced.append(new))

flow.node_types = {"a": NodeType(type="a", schema=_Config)}

assert synced
assert all(isinstance(value["a"], dict) for value in synced)
assert "x" in model.data.node_types["a"]["schema"]["properties"]


def test_node_and_edge_types_updated_together(document, comm) -> None:
flow = ReactFlow()
model = flow.get_root(document, comm=comm)

flow.param.update(
node_types={"a": NodeType(type="a", schema=_Config)},
edge_types={"e": EdgeType(type="e", schema=_Config)},
)

assert isinstance(flow.node_types["a"], dict)
assert isinstance(flow.edge_types["e"], dict)
assert isinstance(model.data.node_types["a"], dict)
assert isinstance(model.data.edge_types["e"], dict)
Loading