panel-reactflow 0.4.0b1__tar.gz → 0.5.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.github/workflows/build.yml +5 -5
  2. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.github/workflows/docs.yml +6 -6
  3. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.github/workflows/test.yml +13 -13
  4. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/PKG-INFO +2 -2
  5. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/declare-types.md +157 -0
  6. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/define-nodes-edges.md +53 -11
  7. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/react-to-events.md +1 -0
  8. panel_reactflow-0.5.0a1/docs/how-to/recover-from-errors.md +121 -0
  9. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/releases.md +77 -0
  10. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/pixi.lock +7439 -5918
  11. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/pixi.toml +2 -2
  12. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/base.py +651 -206
  13. panel_reactflow-0.5.0a1/src/panel_reactflow/dist/css/reactflow.css +233 -0
  14. panel_reactflow-0.5.0a1/src/panel_reactflow/dist/panel-reactflow.bundle.css +1 -0
  15. panel_reactflow-0.5.0a1/src/panel_reactflow/dist/panel-reactflow.bundle.js +88 -0
  16. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/models/reactflow.jsx +496 -46
  17. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/test_api.py +523 -0
  18. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/test_core.py +0 -3
  19. panel_reactflow-0.5.0a1/tests/test_error_recovery.py +128 -0
  20. panel_reactflow-0.5.0a1/tests/ui/test_error_recovery.py +161 -0
  21. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/ui/test_ui.py +218 -1
  22. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/zensical.toml +2 -1
  23. panel_reactflow-0.4.0b1/src/panel_reactflow/dist/css/reactflow.css +0 -90
  24. panel_reactflow-0.4.0b1/src/panel_reactflow/dist/panel-reactflow.bundle.css +0 -1
  25. panel_reactflow-0.4.0b1/src/panel_reactflow/dist/panel-reactflow.bundle.js +0 -88
  26. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.copier-answers.yml +0 -0
  27. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.gitattributes +0 -0
  28. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.github/CODEOWNERS +0 -0
  29. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.github/dependabot.yml +0 -0
  30. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.gitignore +0 -0
  31. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.pre-commit-config.yaml +0 -0
  32. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.prettierrc +0 -0
  33. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/LICENSE.txt +0 -0
  34. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/MANIFEST.in +0 -0
  35. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/README.md +0 -0
  36. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/logo.svg +0 -0
  37. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/declare-types.png +0 -0
  38. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/define-editors-edge.png +0 -0
  39. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/define-editors-node.png +0 -0
  40. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/define-nodes-edges.png +0 -0
  41. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/embed-views-in-nodes.png +0 -0
  42. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/advanced.png +0 -0
  43. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/custom_editor.png +0 -0
  44. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/edge_editors.png +0 -0
  45. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/node_edge_instances.png +0 -0
  46. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/schema_types.png +0 -0
  47. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/simple.png +0 -0
  48. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/threejs_viewer.png +0 -0
  49. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/threejs_viewer_instances.png +0 -0
  50. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/quickstart.png +0 -0
  51. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/react-to-events.png +0 -0
  52. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/style-nodes-edges.png +0 -0
  53. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/advanced.md +0 -0
  54. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/custom-editor.md +0 -0
  55. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/edge-editors.md +0 -0
  56. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/index.md +0 -0
  57. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/node-edge-instances.md +0 -0
  58. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/schema-types.md +0 -0
  59. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/simple.md +0 -0
  60. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/threejs-viewer-instances.md +0 -0
  61. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/threejs-viewer.md +0 -0
  62. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/context-menu.md +0 -0
  63. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/control-handle-connectivity.md +0 -0
  64. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/define-editors.md +0 -0
  65. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/embed-views-in-nodes.md +0 -0
  66. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/style-nodes-edges.md +0 -0
  67. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/index.md +0 -0
  68. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/quickstart.md +0 -0
  69. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/reference/panel_reactflow.md +0 -0
  70. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/advanced.py +0 -0
  71. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/context_menu.py +0 -0
  72. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/custom_editor.py +0 -0
  73. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/edge_editors.py +0 -0
  74. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/edge_types_comparison.py +0 -0
  75. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/node_edge_instances.py +0 -0
  76. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/schema_types.py +0 -0
  77. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/simple.py +0 -0
  78. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/smart_edges_example.py +0 -0
  79. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/threejs_viewer.py +0 -0
  80. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/threejs_viewer_instances.py +0 -0
  81. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/hatch_build.py +0 -0
  82. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/pyproject.toml +0 -0
  83. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/__init__.py +0 -0
  84. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/__version.py +0 -0
  85. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/dist/icons/gear.svg +0 -0
  86. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/py.typed +0 -0
  87. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/schema.py +0 -0
  88. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/__init__.py +0 -0
  89. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/conftest.py +0 -0
  90. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/test_connectable_handles.py +0 -0
  91. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/test_connectable_integration.py +0 -0
  92. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/ui/__init__.py +0 -0
  93. {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/ui/test_context_menu.py +0 -0
@@ -11,11 +11,11 @@ jobs:
11
11
  build:
12
12
  runs-on: ubuntu-latest
13
13
  steps:
14
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
14
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
15
15
  with:
16
16
  fetch-depth: 0
17
17
  - name: Set up pixi
18
- uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4
18
+ uses: prefix-dev/setup-pixi@d3f436a425481402e6a95a1d1fc10331c708cd9e # v0.10.2
19
19
  with:
20
20
  environments: build
21
21
  - name: Build project
@@ -23,7 +23,7 @@ jobs:
23
23
  - name: Check package
24
24
  run: pixi run -e build check-wheel
25
25
  - name: Upload package
26
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
26
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
27
27
  with:
28
28
  name: artifact
29
29
  path: dist/*
@@ -37,9 +37,9 @@ jobs:
37
37
  id-token: write
38
38
  environment: pypi
39
39
  steps:
40
- - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
40
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
41
41
  with:
42
42
  name: artifact
43
43
  path: dist
44
44
  - name: Publish package on PyPi
45
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
45
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -43,23 +43,23 @@ jobs:
43
43
  runs-on: ubuntu-latest
44
44
 
45
45
  steps:
46
- - uses: actions/checkout@v6
46
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
47
47
  with:
48
48
  fetch-depth: 0
49
49
  - name: Set up Pixi
50
- uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4
50
+ uses: prefix-dev/setup-pixi@d3f436a425481402e6a95a1d1fc10331c708cd9e # v0.10.2
51
51
  with:
52
52
  environments: docs
53
53
  - name: Build documentation
54
54
  run: pixi run -e docs docs-build
55
- - uses: actions/upload-artifact@v7
55
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
56
56
  if: always()
57
57
  with:
58
58
  name: docs
59
59
  if-no-files-found: error
60
60
  path: builtdocs
61
61
  - name: Upload artifact
62
- uses: actions/upload-pages-artifact@v4
62
+ uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
63
63
  with:
64
64
  path: ./builtdocs
65
65
 
@@ -71,7 +71,7 @@ jobs:
71
71
  needs: build
72
72
  steps:
73
73
  - name: Deploy dev
74
- uses: peaceiris/actions-gh-pages@v4
74
+ uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
75
75
  # Dev site built on PRs
76
76
  if: |
77
77
  (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
@@ -88,4 +88,4 @@ jobs:
88
88
  ((github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'main') ||
89
89
  (github.event_name == 'push' && !(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))))
90
90
  id: deployment
91
- uses: actions/deploy-pages@v4
91
+ uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
@@ -20,10 +20,10 @@ jobs:
20
20
  code_change: ${{ steps.filter.outputs.code }}
21
21
  matrix: ${{ env.MATRIX }}
22
22
  steps:
23
- - uses: actions/checkout@v6
23
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24
24
  if: github.event_name != 'pull_request'
25
25
  - name: Check for code changes
26
- uses: dorny/paths-filter@v3
26
+ uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
27
27
  id: filter
28
28
  with:
29
29
  filters: |
@@ -72,7 +72,7 @@ jobs:
72
72
  name: Pixi lock
73
73
  runs-on: ubuntu-latest
74
74
  steps:
75
- - uses: holoviz-dev/holoviz_tasks/pixi_lock@v0
75
+ - uses: holoviz-dev/holoviz_tasks/pixi-lock@8fb2d0a3e4c70e218d487e0eed3b9c2488997212 # v1.0.6
76
76
  with:
77
77
  cache: ${{ github.event.inputs.cache == 'true' || github.event.inputs.cache == '' }}
78
78
 
@@ -80,15 +80,15 @@ jobs:
80
80
  needs: [setup, pixi_lock]
81
81
  runs-on: "ubuntu-latest"
82
82
  steps:
83
- - uses: actions/checkout@v6
83
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
84
84
  - name: Set up pixi
85
- uses: prefix-dev/setup-pixi@v0.9.4
86
- - uses: holoviz-dev/holoviz_tasks/pre-commit@v0
87
- - uses: pre-commit/action@v3.0.1
85
+ uses: prefix-dev/setup-pixi@d3f436a425481402e6a95a1d1fc10331c708cd9e # v0.10.2
86
+ - uses: holoviz-dev/holoviz_tasks/pre-commit@8fb2d0a3e4c70e218d487e0eed3b9c2488997212 # v1.0.6
87
+ - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
88
88
  if: needs.setup.outputs.img_change == 'true'
89
89
  with:
90
90
  extra_args: -a --hook-stage manual oxipng || true --
91
- - uses: stefanzweifel/git-auto-commit-action@v7
91
+ - uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0
92
92
  if: needs.setup.outputs.img_change == 'true'
93
93
  with:
94
94
  commit_message: "Optimize PNG images (lossless)"
@@ -111,11 +111,11 @@ jobs:
111
111
  - windows-latest
112
112
  steps:
113
113
  - name: Checkout branch
114
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
114
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
115
115
  with:
116
116
  fetch-depth: 0
117
117
  - name: Set up pixi
118
- uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4
118
+ uses: prefix-dev/setup-pixi@d3f436a425481402e6a95a1d1fc10331c708cd9e # v0.10.2
119
119
  with:
120
120
  environments: ${{ matrix.environment }}
121
121
  - name: Install repository
@@ -139,7 +139,7 @@ jobs:
139
139
  PANEL_LOG_LEVEL: info
140
140
  FAIL: "--screenshot only-on-failure --full-page-screenshot --output ui_screenshots --tracing retain-on-failure"
141
141
  steps:
142
- - uses: holoviz-dev/holoviz_tasks/pixi_install@v0
142
+ - uses: holoviz-dev/holoviz_tasks/pixi-install@8fb2d0a3e4c70e218d487e0eed3b9c2488997212 # v1.0.6
143
143
  with:
144
144
  environments: ${{ matrix.environment }}
145
145
  install: false
@@ -151,14 +151,14 @@ jobs:
151
151
  echo "[run]\nconcurrency = greenlet" > .uicoveragerc
152
152
  pixi run -e ${{ matrix.environment }} test-ui $COV --cov-config=.uicoveragerc $FAIL
153
153
  - name: Upload UI Screenshots
154
- uses: actions/upload-artifact@v7
154
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
155
155
  if: always()
156
156
  with:
157
157
  name: ui_screenshots_${{ runner.os }}
158
158
  path: ./ui_screenshots
159
159
  if-no-files-found: ignore
160
160
  - name: Upload coverage reports to Codecov
161
- uses: codecov/codecov-action@v5
161
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
162
162
  with:
163
163
  token: ${{ secrets.CODECOV_TOKEN }}
164
164
  slug: panel-extensions/panel-splitjs
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: panel-reactflow
3
- Version: 0.4.0b1
3
+ Version: 0.5.0a1
4
4
  Summary: A Panel wrapper for the Reactflow JS library.
5
5
  Project-URL: Homepage, https://github.com/panel-extensions/panel-reactflow
6
6
  Project-URL: Source, https://github.com/panel-extensions/panel-reactflow
@@ -6,6 +6,7 @@ kind of node/edge carries**. A type can provide:
6
6
  - a type name (`type`)
7
7
  - a display label (`label`)
8
8
  - node handles (`inputs` / `outputs`)
9
+ - handle connectivity controls (`input_connectable*` / `output_connectable*`)
9
10
  - a schema for the `data` payload (`schema`)
10
11
 
11
12
  Types are separate from editors. A type defines structure; an editor defines
@@ -237,3 +238,159 @@ flow = ReactFlow(
237
238
 
238
239
  Types without a schema still work; they just do not get schema-driven
239
240
  validation or auto-generated forms.
241
+
242
+ ---
243
+
244
+ ## Handle tooltips
245
+
246
+ By default, handles are plain connection points. You can add a tooltip (shown
247
+ on hover) by passing a dict with `"id"` and `"label"` instead of a plain string:
248
+
249
+ ```python
250
+ from panel_reactflow import NodeType
251
+
252
+ node_types = {
253
+ "transform": NodeType(
254
+ type="transform",
255
+ label="Transform",
256
+ inputs=[{"id": "in", "label": "Data Input"}],
257
+ outputs=[
258
+ {"id": "success", "label": "Successful results"},
259
+ {"id": "error", "label": "Failed records"},
260
+ ],
261
+ ),
262
+ }
263
+ ```
264
+
265
+ Plain strings and dicts can be mixed freely in the same list:
266
+
267
+ ```python
268
+ inputs=["simple_port", {"id": "documented_port", "label": "Hover to see this"}]
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Control handle connectivity
274
+
275
+ By default, all handles (inputs and outputs) are fully connectable — users can
276
+ drag edges from or to any handle. Use the `*_connectable*` flags to restrict
277
+ which connections are allowed.
278
+
279
+ ### Common patterns
280
+
281
+ #### Data source (output only)
282
+
283
+ A node that produces data but cannot accept incoming connections to its output:
284
+
285
+ ```python
286
+ from panel_reactflow import NodeType
287
+
288
+ source_type = NodeType(
289
+ type="data_source",
290
+ label="Data Source",
291
+ outputs=["data"],
292
+ output_connectable_start=True, # Can drag FROM output
293
+ output_connectable_end=False, # Cannot drag TO output
294
+ )
295
+ ```
296
+
297
+ #### Data sink (input only)
298
+
299
+ A node that consumes data but cannot produce outgoing connections from its input:
300
+
301
+ ```python
302
+ sink_type = NodeType(
303
+ type="data_sink",
304
+ label="Data Sink",
305
+ inputs=["data"],
306
+ input_connectable_start=False, # Cannot drag FROM input
307
+ input_connectable_end=True, # Can drag TO input
308
+ )
309
+ ```
310
+
311
+ #### Monitor node
312
+
313
+ A node that accepts input but whose output is status-only (one direction):
314
+
315
+ ```python
316
+ monitor_type = NodeType(
317
+ type="monitor",
318
+ label="Monitor",
319
+ inputs=["in"],
320
+ outputs=["status"],
321
+ input_connectable_start=False, # Cannot start edges from input
322
+ output_connectable_end=False, # Cannot end edges at output
323
+ )
324
+ ```
325
+
326
+ ### All connectivity flags
327
+
328
+ | Flag | Default | Controls |
329
+ |------|---------|----------|
330
+ | `input_connectable` | `True` | Whether input handles are connectable at all |
331
+ | `input_connectable_start` | `True` | Whether edges can start from input handles |
332
+ | `input_connectable_end` | `True` | Whether edges can end at input handles |
333
+ | `output_connectable` | `True` | Whether output handles are connectable at all |
334
+ | `output_connectable_start` | `True` | Whether edges can start from output handles |
335
+ | `output_connectable_end` | `True` | Whether edges can end at output handles |
336
+
337
+ ### Complete example
338
+
339
+ ```python
340
+ import panel as pn
341
+ from panel_reactflow import NodeType, NodeSpec, EdgeSpec, ReactFlow
342
+
343
+ pn.extension("jsoneditor")
344
+
345
+ # Define node types with different connectivity patterns
346
+ node_types = {
347
+ "source": NodeType(
348
+ type="source",
349
+ label="Data Source",
350
+ outputs=["data"],
351
+ output_connectable_start=True,
352
+ output_connectable_end=False,
353
+ ),
354
+ "transform": NodeType(
355
+ type="transform",
356
+ label="Transform",
357
+ inputs=["in"],
358
+ outputs=["out"],
359
+ # All connectable flags default to True
360
+ ),
361
+ "sink": NodeType(
362
+ type="sink",
363
+ label="Data Sink",
364
+ inputs=["data"],
365
+ input_connectable_start=False,
366
+ input_connectable_end=True,
367
+ ),
368
+ }
369
+
370
+ # Create a data pipeline
371
+ flow = ReactFlow(
372
+ nodes=[
373
+ NodeSpec(id="src", type="source", position={"x": 0, "y": 100}, data={}).to_dict(),
374
+ NodeSpec(id="tx", type="transform", position={"x": 250, "y": 100}, data={}).to_dict(),
375
+ NodeSpec(id="snk", type="sink", position={"x": 500, "y": 100}, data={}).to_dict(),
376
+ ],
377
+ edges=[
378
+ EdgeSpec(id="e1", source="src", target="tx").to_dict(),
379
+ EdgeSpec(id="e2", source="tx", target="snk").to_dict(),
380
+ ],
381
+ node_types=node_types,
382
+ sizing_mode="stretch_both",
383
+ )
384
+
385
+ flow.servable()
386
+ ```
387
+
388
+ In this example:
389
+
390
+ - Users can drag from the **source** output to the **transform** input ✓
391
+ - Users cannot drag to the **source** output ✗
392
+ - Users can drag from the **transform** output to the **sink** input ✓
393
+ - Users cannot drag from the **sink** input ✗
394
+
395
+ The UI prevents invalid connections automatically — non-connectable handles
396
+ show different cursor behavior and won't accept drag operations.
@@ -358,26 +358,45 @@ Without `sourceHandle` and `targetHandle`, edges connect to the default (first)
358
358
 
359
359
  ---
360
360
 
361
- ## Update data vs. label
361
+ ## Update data vs. presentation
362
362
 
363
- `data` and `label` live in different places and are updated differently:
364
-
365
- - **Data** — use `patch_node_data()` or `patch_edge_data()`. This sends
366
- an incremental patch to the frontend without replacing the full list.
367
- - **Label** — replace the node/edge in `flow.nodes` or `flow.edges`.
363
+ A node or edge has an arbitrary `data` dictionary plus a set of top-level React
364
+ Flow fields (`label`, `style`, `type`, `className`, `position`, ...). Each has
365
+ its own patch method, and both send an incremental update to the frontend
366
+ instead of replacing the full list:
368
367
 
369
368
  ```python
370
369
  # Patch a data field
371
370
  flow.patch_node_data("n1", {"status": "running"})
372
371
  flow.patch_edge_data("e1", {"weight": 0.75})
373
372
 
374
- # Update a label
375
- flow.nodes = [
376
- {**node, "label": "Start (running)"} if node["id"] == "n1" else node
377
- for node in flow.nodes
378
- ]
373
+ # Patch a top-level field
374
+ flow.patch_node_props("n1", {"label": "Start (running)", "className": "busy"})
375
+ flow.patch_edge_props("e1", {"style": {"stroke": "#ef4444", "strokeWidth": 4}, "type": "step"})
376
+ ```
377
+
378
+ Passing `None` to `patch_node_props()`/`patch_edge_props()` clears the field so
379
+ the element falls back to the CSS/theme default:
380
+
381
+ ```python
382
+ flow.patch_edge_props("e1", {"style": None})
379
383
  ```
380
384
 
385
+ If you use `Node`/`Edge` subclasses, you rarely need either method: assigning to
386
+ a parameter patches the browser in place. Parameters you declare on the
387
+ subclass are synced into `data`, the presentational base parameters are synced
388
+ as top-level fields.
389
+
390
+ ```python
391
+ node.label = "Start (running)" # top-level label
392
+ edge.style = {"stroke": "#ef4444"} # top-level style
393
+ edge.weight = 0.75 # subclass param, goes into data
394
+ ```
395
+
396
+ `position` and `selected` are the exception: the browser owns them while the
397
+ user drags or selects, so assignment does not push them. Use
398
+ `patch_node_props()` to move or select a node from Python.
399
+
381
400
  ---
382
401
 
383
402
  ## Add and remove at runtime
@@ -399,3 +418,26 @@ flow.add_edge(EdgeSpec(id="e2", source="n3", target="n4"))
399
418
  flow.remove_node("n3") # also removes connected edges
400
419
  flow.remove_edge("e1")
401
420
  ```
421
+
422
+ `remove_node()` and `remove_edge()` also take several ids at once, either as
423
+ separate arguments or as a single sequence. Pass them together rather than
424
+ calling the methods in a loop: a batch assigns `nodes` and `edges` once, so the
425
+ browser renders a single update instead of one per element.
426
+
427
+ ```python
428
+ flow.remove_node("n1", "n2", "n3") # also removes connected edges
429
+ flow.remove_edge(["e1", "e2"])
430
+ ```
431
+
432
+ More generally, every parameter assignment is synced to the browser on its own
433
+ and the canvas re-renders per sync, so a sequence of updates renders each
434
+ intermediate graph. Wrap any batch of changes in `pn.io.hold()` to combine them
435
+ into a single render:
436
+
437
+ ```python
438
+ import panel as pn
439
+
440
+ with pn.io.hold():
441
+ flow.nodes = new_nodes
442
+ flow.edges = new_edges
443
+ ```
@@ -30,6 +30,7 @@ the `ReactFlow` instance as a second argument. You can also listen for
30
30
  | `edge_data_changed` | Edge data is patched (via API, editor patch, or parameter-driven sync). | `edge_id`, `patch` |
31
31
  | `selection_changed` | The active selection changes. | `nodes`, `edges` |
32
32
  | `sync` | A batch sync from the frontend. | *(varies)* |
33
+ | `client_error` | The graph view hit a rendering error in the browser. See [Recover from Rendering Errors](recover-from-errors.md). | `source`, `message`, `stack`, `component_stack`, `attempt`, `mode` |
33
34
 
34
35
  ---
35
36
 
@@ -0,0 +1,121 @@
1
+ # Recover from Rendering Errors
2
+
3
+ A React rendering error is unforgiving: when a component throws during
4
+ render, React unmounts the whole subtree. In a graph editor that means a
5
+ single malformed node can blank the canvas, and because the exception dies
6
+ in the browser console the server never learns about it. The user is left
7
+ staring at an empty viewport with no way back other than reloading the page,
8
+ even though their graph is still safely held in Python.
9
+
10
+ Panel-ReactFlow wraps the canvas in an error boundary that catches those
11
+ errors, tries to bring the view back, and reports what happened to the
12
+ server. This is on by default, controlled by the `error_recovery`
13
+ parameter.
14
+
15
+ ```python
16
+ from panel_reactflow import ReactFlow
17
+
18
+ flow = ReactFlow(nodes=nodes, edges=edges, error_recovery="auto")
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Recovery modes
24
+
25
+ | Mode | Behavior |
26
+ |------------|----------|
27
+ | `"auto"` | *(default)* Remount the canvas once, then remount again in safe mode. If it still fails, show the recovery panel. |
28
+ | `"manual"` | Report the error and show the recovery panel immediately, without retrying. |
29
+ | `"off"` | Disable the error boundary entirely so exceptions propagate to the browser. Useful when debugging a custom node component. |
30
+
31
+ Each retry budget refills once a remounted canvas has survived for five
32
+ seconds, so a graph that breaks again much later still gets a fresh set of
33
+ attempts rather than going straight to the failure panel.
34
+
35
+ ---
36
+
37
+ ## What safe mode does
38
+
39
+ On the second attempt the frontend validates the graph before handing it to
40
+ React Flow and either repairs or hides anything it cannot render:
41
+
42
+ | Issue | Action |
43
+ |-------|--------|
44
+ | `invalid_position` | Position is missing or not finite, so the node is placed at the origin. |
45
+ | `unknown_node_type` | Node type is not registered, so the node falls back to the default renderer. |
46
+ | `unknown_edge_type` | Edge type is not registered, so the type is stripped. |
47
+ | `dangling_edge` | Edge references a node that does not exist, so it is hidden. |
48
+ | `duplicate_node_id` / `duplicate_edge_id` | Later duplicates are hidden. |
49
+ | `missing_node_id` / `missing_edge_id` / `invalid_node` / `invalid_edge` | The element is hidden. |
50
+
51
+ Safe mode is **view-only**. It filters what the browser renders and never
52
+ sends a graph mutation back to Python, so `flow.nodes` and `flow.edges` keep
53
+ every element they had before the error. Once the underlying state is
54
+ repaired on the server, the affected elements reappear.
55
+
56
+ A banner tells the user what was changed and offers a details list of the
57
+ individual issues:
58
+
59
+ ```text
60
+ Safe mode: repaired 1 element and hid 1 element that could not be rendered.
61
+ Nothing was deleted on the server.
62
+ ```
63
+
64
+ ---
65
+
66
+ ## The recovery panel
67
+
68
+ When retries are exhausted, or in `"manual"` mode, the canvas is replaced by
69
+ a panel that names the error and offers three actions: *Try again*, which
70
+ remounts the canvas, *Reload page*, which rebuilds the session from the
71
+ server-side state, and *Copy details*, which puts a JSON diagnostic blob on
72
+ the clipboard for a bug report.
73
+
74
+ Because Python holds the canonical graph, reloading is genuinely safe: no
75
+ work is lost. The panel says so explicitly, which matters when the
76
+ alternative is a user assuming their graph is gone.
77
+
78
+ ---
79
+
80
+ ## Log and handle errors in Python
81
+
82
+ Every error the frontend catches is reported to the server, logged to the
83
+ `panel.reactflow` logger, and emitted as a `client_error` event.
84
+
85
+ ```python
86
+ import logging
87
+
88
+ logging.getLogger("panel.reactflow").setLevel(logging.INFO)
89
+
90
+ def on_client_error(payload, flow):
91
+ if payload["source"] == "safe_mode":
92
+ print("hidden or repaired:", payload["issues"])
93
+ else:
94
+ print(f"render error on attempt {payload['attempt']}: {payload['message']}")
95
+
96
+ flow.on("client_error", on_client_error)
97
+ ```
98
+
99
+ Render errors carry `name`, `message`, `stack`, `component_stack`, `attempt`,
100
+ `mode` and `auto_retry`. Errors raised inside interaction handlers are
101
+ reported with `source="handler"` and the `handler` name, which catches the
102
+ case where a drag or connect silently fails and leaves the canvas showing a
103
+ change that never reached Python. Safe mode reports arrive with
104
+ `source="safe_mode"` and the list of `issues`.
105
+
106
+ Use this hook to forward errors to your own telemetry, to snapshot the graph
107
+ for later inspection, or to attempt a server-side repair before the user
108
+ clicks *Try again*.
109
+
110
+ ---
111
+
112
+ ## Tips
113
+
114
+ - Keep `error_recovery="auto"` in production; switch to `"off"` while
115
+ developing a custom node component so you see the real stack trace.
116
+ - A `client_error` with `source="safe_mode"` is a strong signal that
117
+ something upstream produced invalid state. Treat it as a bug report
118
+ rather than a warning to be ignored.
119
+ - The error boundary only covers the graph canvas. Content you pass to
120
+ `top_panel`, `bottom_panel`, `left_panel` and `right_panel` stays mounted
121
+ when the canvas fails, so side panels remain usable during recovery.
@@ -1,5 +1,82 @@
1
1
  # Release Notes
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Bug fixes
6
+
7
+ - **Progressive re-render when deleting multiple elements** — deleting a
8
+ multi-node selection removed the nodes one at a time, syncing an
9
+ intermediate graph to the browser per node so the nodes visibly
10
+ disappeared one by one. Updates triggered by a frontend message are now
11
+ held and combined into a single patch, and node/edge deletion assigns
12
+ `nodes` and `edges` once.
13
+
14
+ ### Enhancements
15
+
16
+ - **Batch removal** — `remove_node()` and `remove_edge()` now accept
17
+ several ids, either as separate arguments
18
+ (`flow.remove_node("n1", "n2")`) or as a sequence
19
+ (`flow.remove_node(["n1", "n2"])`), and remove them in a single update.
20
+ For any other batch of changes made from Python, wrap them in
21
+ `pn.io.hold()` to render them at once.
22
+
23
+ ## Version 0.4.1
24
+
25
+ A small enhancement release adding viewport zoom controls.
26
+
27
+ ### Enhancements
28
+
29
+ - **Zoom limits** — new `min_zoom` (default `0.5`) and `max_zoom`
30
+ (default `2`) parameters constrain how far the canvas can be zoomed
31
+ out and in, and can be updated at runtime
32
+ ([#69](https://github.com/panel-extensions/panel-reactflow/pull/69)).
33
+
34
+ ## Version 0.4.0
35
+
36
+ This release adds new interaction features, edge routing options, and
37
+ handle customization capabilities.
38
+
39
+ ### Highlights
40
+
41
+ - **Right-click context menu** — nodes and the canvas now support a
42
+ configurable context menu triggered on right-click, enabling custom
43
+ actions like delete, duplicate, or copy without toolbar buttons
44
+ ([#65](https://github.com/panel-extensions/panel-reactflow/pull/65)).
45
+ - **Smart edge routing** — new smart edge types (`smartBezier`,
46
+ `smartStep`, `smartStraight`) that automatically route around nodes
47
+ instead of passing through them
48
+ ([#64](https://github.com/panel-extensions/panel-reactflow/pull/64)).
49
+ - **Handle connectivity controls** — per-type flags
50
+ (`input_connectable`, `input_connectable_start`,
51
+ `input_connectable_end`, and their `output_*` counterparts) let you
52
+ restrict which handles users can drag edges from or to
53
+ ([#63](https://github.com/panel-extensions/panel-reactflow/pull/63)).
54
+ - **Handle tooltips** — handles can now carry a `label` that appears as
55
+ an instant tooltip on hover, making port purpose discoverable without
56
+ cluttering the canvas. Pass `{"id": "port_name", "label": "Description"}`
57
+ in your `inputs`/`outputs` lists
58
+ ([#67](https://github.com/panel-extensions/panel-reactflow/pull/67)).
59
+ - **`Node.flow` and `Edge.flow` back-references** — instance-based nodes
60
+ and edges now hold a `.flow` reference to their parent `ReactFlow`,
61
+ simplifying imperative graph manipulation from callbacks
62
+ ([#56](https://github.com/panel-extensions/panel-reactflow/pull/56)).
63
+ - **Delete key support** — selected nodes and edges can now be removed
64
+ with the Delete key, matching standard graph-editor UX expectations.
65
+ - **Bundle cache-busting** — the frontend bundle URL now includes a
66
+ content hash, preventing stale cached bundles after upgrades
67
+ ([#68](https://github.com/panel-extensions/panel-reactflow/pull/68)).
68
+
69
+ ### Bug fixes
70
+
71
+ - Fixed stale node views not updating and deletion causing flicker
72
+ ([#60](https://github.com/panel-extensions/panel-reactflow/pull/60)).
73
+ - Fixed `flow` attribute initialization on component creation
74
+ ([#59](https://github.com/panel-extensions/panel-reactflow/pull/59)).
75
+ - Edge/node add/remove events are now emitted exactly once
76
+ ([#58](https://github.com/panel-extensions/panel-reactflow/pull/58)).
77
+ - Fixed removing a node when edges are defined as `Edge` instances
78
+ ([#57](https://github.com/panel-extensions/panel-reactflow/pull/57)).
79
+
3
80
  ## Version 0.3.0
4
81
 
5
82
  This release focuses on core graph-model capabilities, callback ergonomics,