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.
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.github/workflows/build.yml +5 -5
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.github/workflows/docs.yml +6 -6
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.github/workflows/test.yml +13 -13
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/PKG-INFO +2 -2
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/declare-types.md +157 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/define-nodes-edges.md +53 -11
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/react-to-events.md +1 -0
- panel_reactflow-0.5.0a1/docs/how-to/recover-from-errors.md +121 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/releases.md +77 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/pixi.lock +7439 -5918
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/pixi.toml +2 -2
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/base.py +651 -206
- panel_reactflow-0.5.0a1/src/panel_reactflow/dist/css/reactflow.css +233 -0
- panel_reactflow-0.5.0a1/src/panel_reactflow/dist/panel-reactflow.bundle.css +1 -0
- panel_reactflow-0.5.0a1/src/panel_reactflow/dist/panel-reactflow.bundle.js +88 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/models/reactflow.jsx +496 -46
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/test_api.py +523 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/test_core.py +0 -3
- panel_reactflow-0.5.0a1/tests/test_error_recovery.py +128 -0
- panel_reactflow-0.5.0a1/tests/ui/test_error_recovery.py +161 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/ui/test_ui.py +218 -1
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/zensical.toml +2 -1
- panel_reactflow-0.4.0b1/src/panel_reactflow/dist/css/reactflow.css +0 -90
- panel_reactflow-0.4.0b1/src/panel_reactflow/dist/panel-reactflow.bundle.css +0 -1
- panel_reactflow-0.4.0b1/src/panel_reactflow/dist/panel-reactflow.bundle.js +0 -88
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.copier-answers.yml +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.gitattributes +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.github/CODEOWNERS +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.github/dependabot.yml +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.gitignore +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.pre-commit-config.yaml +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/.prettierrc +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/LICENSE.txt +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/MANIFEST.in +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/README.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/logo.svg +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/declare-types.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/define-editors-edge.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/define-editors-node.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/define-nodes-edges.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/embed-views-in-nodes.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/advanced.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/custom_editor.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/edge_editors.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/node_edge_instances.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/schema_types.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/simple.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/threejs_viewer.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/examples/threejs_viewer_instances.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/quickstart.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/react-to-events.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/assets/screenshots/style-nodes-edges.png +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/advanced.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/custom-editor.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/edge-editors.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/index.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/node-edge-instances.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/schema-types.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/simple.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/threejs-viewer-instances.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/examples/threejs-viewer.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/context-menu.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/control-handle-connectivity.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/define-editors.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/embed-views-in-nodes.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/how-to/style-nodes-edges.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/index.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/quickstart.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/docs/reference/panel_reactflow.md +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/advanced.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/context_menu.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/custom_editor.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/edge_editors.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/edge_types_comparison.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/node_edge_instances.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/schema_types.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/simple.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/smart_edges_example.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/threejs_viewer.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/examples/threejs_viewer_instances.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/hatch_build.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/pyproject.toml +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/__init__.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/__version.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/dist/icons/gear.svg +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/py.typed +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/src/panel_reactflow/schema.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/__init__.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/conftest.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/test_connectable_handles.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/test_connectable_integration.py +0 -0
- {panel_reactflow-0.4.0b1 → panel_reactflow-0.5.0a1}/tests/ui/__init__.py +0 -0
- {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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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/
|
|
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@
|
|
83
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
84
84
|
- name: Set up pixi
|
|
85
|
-
uses: prefix-dev/setup-pixi@v0.
|
|
86
|
-
- uses: holoviz-dev/holoviz_tasks/pre-commit@
|
|
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@
|
|
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@
|
|
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/
|
|
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@
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: panel-reactflow
|
|
3
|
-
Version: 0.
|
|
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.
|
|
361
|
+
## Update data vs. presentation
|
|
362
362
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
#
|
|
375
|
-
flow.
|
|
376
|
-
|
|
377
|
-
|
|
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,
|