sp-reflex-components 0.2.0__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.
- sp_reflex_components-0.2.0/.github/workflows/release.yml +62 -0
- sp_reflex_components-0.2.0/.gitignore +7 -0
- sp_reflex_components-0.2.0/LICENSE +202 -0
- sp_reflex_components-0.2.0/PKG-INFO +94 -0
- sp_reflex_components-0.2.0/README.md +77 -0
- sp_reflex_components-0.2.0/pyproject.toml +42 -0
- sp_reflex_components-0.2.0/src/sp_reflex_components/__init__.py +48 -0
- sp_reflex_components-0.2.0/src/sp_reflex_components/ag_grid.py +231 -0
- sp_reflex_components-0.2.0/src/sp_reflex_components/react_flow.py +95 -0
- sp_reflex_components-0.2.0/tests/test_wrappers.py +103 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tag vX.Y.Z → test, build, publish to PyPI (trusted publishing / OIDC — no
|
|
4
|
+
# API token secret), and attach the artifacts to a GitHub release.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v5
|
|
15
|
+
- name: Test
|
|
16
|
+
run: |
|
|
17
|
+
uv sync
|
|
18
|
+
uv run pytest -q
|
|
19
|
+
uv run ruff check src tests
|
|
20
|
+
uv run ruff format --check .
|
|
21
|
+
- name: Build
|
|
22
|
+
run: |
|
|
23
|
+
uv build
|
|
24
|
+
sha256sum dist/* > SHA256SUMS && mv SHA256SUMS dist/
|
|
25
|
+
- uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish-pypi:
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
environment: pypi
|
|
34
|
+
permissions:
|
|
35
|
+
id-token: write # PyPI trusted publishing (OIDC)
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/download-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist/
|
|
41
|
+
- name: Remove non-distributable files
|
|
42
|
+
run: rm -f dist/SHA256SUMS
|
|
43
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
44
|
+
|
|
45
|
+
github-release:
|
|
46
|
+
needs: build
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
permissions:
|
|
49
|
+
contents: write
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/download-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: dist
|
|
54
|
+
path: dist/
|
|
55
|
+
- name: Release
|
|
56
|
+
env:
|
|
57
|
+
GH_TOKEN: ${{ github.token }}
|
|
58
|
+
run: |
|
|
59
|
+
gh release create "${GITHUB_REF_NAME}" dist/* \
|
|
60
|
+
--repo "${GITHUB_REPOSITORY}" \
|
|
61
|
+
--title "${GITHUB_REF_NAME}" \
|
|
62
|
+
--notes "Published to PyPI as sp-reflex-components ${GITHUB_REF_NAME#v}. Wheel + sha256 also attached here."
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sp-reflex-components
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Reflex component wrappers for AG Grid (community + enterprise modules) and React Flow - self-host on core Reflex without reflex-enterprise.
|
|
5
|
+
Project-URL: Repository, https://github.com/MIM-SP/sp-reflex-components
|
|
6
|
+
Project-URL: Issues, https://github.com/MIM-SP/sp-reflex-components/issues
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: ag-grid,components,react-flow,reflex
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Requires-Dist: reflex<0.10,>=0.9.5
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# sp-reflex-components
|
|
19
|
+
|
|
20
|
+
Reflex component wrappers: **AG Grid** (ag-grid-react +
|
|
21
|
+
community + enterprise modules) and **React Flow** (@xyflow/react). These
|
|
22
|
+
replace `reflex-enterprise`'s component wrappers so apps self-host on core
|
|
23
|
+
Reflex (Apache-2.0) with **no Reflex tier, token, badge, or reflex.dev
|
|
24
|
+
dependency**.
|
|
25
|
+
|
|
26
|
+
Built and maintained by Solid Power for internal Reflex apps; published in
|
|
27
|
+
case others hit the same wall. Apache-2.0.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add sp-reflex-components
|
|
33
|
+
# or: pip install sp-reflex-components
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from sp_reflex_components import grid, flow
|
|
40
|
+
|
|
41
|
+
# AG Grid — drop-in for reflex_enterprise's ag_grid.root(...)
|
|
42
|
+
grid(
|
|
43
|
+
id="my-grid",
|
|
44
|
+
row_data=State.rows,
|
|
45
|
+
column_defs=[{"field": "name", "header_name": "Name", "editable": True}],
|
|
46
|
+
on_selection_changed=State.on_select, # (rows, source, type)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# React Flow — drop-in for reflex_enterprise's flow(...)
|
|
50
|
+
flow(
|
|
51
|
+
flow.controls(),
|
|
52
|
+
flow.background(variant="dots", gap=18, size=1),
|
|
53
|
+
nodes=State.nodes,
|
|
54
|
+
edges=State.edges,
|
|
55
|
+
fit_view=True,
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
AG Grid Enterprise features (clipboard, cell selection) need
|
|
60
|
+
`AG_GRID_LICENSE_KEY` in the environment **at compile time** — that is AG
|
|
61
|
+
Grid's own per-developer license, unrelated to Reflex. Without it the grid
|
|
62
|
+
runs in trial mode with a watermark.
|
|
63
|
+
|
|
64
|
+
## Scope doctrine
|
|
65
|
+
|
|
66
|
+
- A wrapper exposes **exactly** the prop/event surface consuming apps use —
|
|
67
|
+
no speculative props. Need a new prop? Add it here **with a test**, in the
|
|
68
|
+
same PR that uses it; every app inherits it.
|
|
69
|
+
- npm versions are **pinned exactly** in this package. A bump is a deliberate
|
|
70
|
+
release with a changelog read (AG Grid majors change behavior: v35 added
|
|
71
|
+
no-matching-rows/exporting overlays, v36 overhauled the DOM containers).
|
|
72
|
+
- Legacy CSS theming (`ag-theme-alpine` + `theme: "legacy"`) is deprecated
|
|
73
|
+
upstream; some future AG Grid major will force a Theming API migration —
|
|
74
|
+
that lands here once, for everyone.
|
|
75
|
+
- Each consuming app should keep an adapter/wrapper **prop-equality test**
|
|
76
|
+
(both directions, derived from a real render) — see ATLAS's
|
|
77
|
+
`tests/unit/reflex_runtime/components/test_vendor_ag_grid.py` for the
|
|
78
|
+
pattern.
|
|
79
|
+
|
|
80
|
+
## Known issues
|
|
81
|
+
|
|
82
|
+
- React Flow renders nodes but **edge rendering has an unresolved defect**
|
|
83
|
+
(edges reach the ReactFlow component as props but no edge elements render;
|
|
84
|
+
reproduces identically under reflex-enterprise's own wrapper on
|
|
85
|
+
@xyflow/react 12.8.4 and 12.11.2, so it is not specific to this package).
|
|
86
|
+
Root cause TBD; issues/PRs welcome.
|
|
87
|
+
|
|
88
|
+
## Dev
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv sync
|
|
92
|
+
uv run pytest -q
|
|
93
|
+
uv run ruff check src tests && uv run ruff format --check .
|
|
94
|
+
```
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# sp-reflex-components
|
|
2
|
+
|
|
3
|
+
Reflex component wrappers: **AG Grid** (ag-grid-react +
|
|
4
|
+
community + enterprise modules) and **React Flow** (@xyflow/react). These
|
|
5
|
+
replace `reflex-enterprise`'s component wrappers so apps self-host on core
|
|
6
|
+
Reflex (Apache-2.0) with **no Reflex tier, token, badge, or reflex.dev
|
|
7
|
+
dependency**.
|
|
8
|
+
|
|
9
|
+
Built and maintained by Solid Power for internal Reflex apps; published in
|
|
10
|
+
case others hit the same wall. Apache-2.0.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv add sp-reflex-components
|
|
16
|
+
# or: pip install sp-reflex-components
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Use
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from sp_reflex_components import grid, flow
|
|
23
|
+
|
|
24
|
+
# AG Grid — drop-in for reflex_enterprise's ag_grid.root(...)
|
|
25
|
+
grid(
|
|
26
|
+
id="my-grid",
|
|
27
|
+
row_data=State.rows,
|
|
28
|
+
column_defs=[{"field": "name", "header_name": "Name", "editable": True}],
|
|
29
|
+
on_selection_changed=State.on_select, # (rows, source, type)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# React Flow — drop-in for reflex_enterprise's flow(...)
|
|
33
|
+
flow(
|
|
34
|
+
flow.controls(),
|
|
35
|
+
flow.background(variant="dots", gap=18, size=1),
|
|
36
|
+
nodes=State.nodes,
|
|
37
|
+
edges=State.edges,
|
|
38
|
+
fit_view=True,
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
AG Grid Enterprise features (clipboard, cell selection) need
|
|
43
|
+
`AG_GRID_LICENSE_KEY` in the environment **at compile time** — that is AG
|
|
44
|
+
Grid's own per-developer license, unrelated to Reflex. Without it the grid
|
|
45
|
+
runs in trial mode with a watermark.
|
|
46
|
+
|
|
47
|
+
## Scope doctrine
|
|
48
|
+
|
|
49
|
+
- A wrapper exposes **exactly** the prop/event surface consuming apps use —
|
|
50
|
+
no speculative props. Need a new prop? Add it here **with a test**, in the
|
|
51
|
+
same PR that uses it; every app inherits it.
|
|
52
|
+
- npm versions are **pinned exactly** in this package. A bump is a deliberate
|
|
53
|
+
release with a changelog read (AG Grid majors change behavior: v35 added
|
|
54
|
+
no-matching-rows/exporting overlays, v36 overhauled the DOM containers).
|
|
55
|
+
- Legacy CSS theming (`ag-theme-alpine` + `theme: "legacy"`) is deprecated
|
|
56
|
+
upstream; some future AG Grid major will force a Theming API migration —
|
|
57
|
+
that lands here once, for everyone.
|
|
58
|
+
- Each consuming app should keep an adapter/wrapper **prop-equality test**
|
|
59
|
+
(both directions, derived from a real render) — see ATLAS's
|
|
60
|
+
`tests/unit/reflex_runtime/components/test_vendor_ag_grid.py` for the
|
|
61
|
+
pattern.
|
|
62
|
+
|
|
63
|
+
## Known issues
|
|
64
|
+
|
|
65
|
+
- React Flow renders nodes but **edge rendering has an unresolved defect**
|
|
66
|
+
(edges reach the ReactFlow component as props but no edge elements render;
|
|
67
|
+
reproduces identically under reflex-enterprise's own wrapper on
|
|
68
|
+
@xyflow/react 12.8.4 and 12.11.2, so it is not specific to this package).
|
|
69
|
+
Root cause TBD; issues/PRs welcome.
|
|
70
|
+
|
|
71
|
+
## Dev
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv sync
|
|
75
|
+
uv run pytest -q
|
|
76
|
+
uv run ruff check src tests && uv run ruff format --check .
|
|
77
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sp-reflex-components"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Reflex component wrappers for AG Grid (community + enterprise modules) and React Flow - self-host on core Reflex without reflex-enterprise."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
keywords = ["reflex", "ag-grid", "react-flow", "components"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Topic :: Software Development :: User Interfaces",
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"reflex>=0.9.5,<0.10",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8",
|
|
23
|
+
"ruff>=0.8",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/sp_reflex_components"]
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
line-length = 100
|
|
35
|
+
src = ["src", "tests"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Repository = "https://github.com/MIM-SP/sp-reflex-components"
|
|
42
|
+
Issues = "https://github.com/MIM-SP/sp-reflex-components/issues"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Solid Power shared Reflex component wrappers.
|
|
2
|
+
|
|
3
|
+
Own wrappers over the same npm packages reflex-enterprise wrapped — AG Grid
|
|
4
|
+
(ag-grid-react + community + enterprise modules) and React Flow
|
|
5
|
+
(@xyflow/react) — with no Reflex tier, token, or badge requirements.
|
|
6
|
+
|
|
7
|
+
Scope doctrine (from ATLAS SPEC §V64): a wrapper exposes exactly the prop and
|
|
8
|
+
event surface consuming apps actually use. Need a new prop? Add it HERE with a
|
|
9
|
+
test, in the same PR that uses it — every consuming app then inherits it.
|
|
10
|
+
Never add speculative props. npm versions are pinned exactly in this package;
|
|
11
|
+
a bump is a deliberate, changelog-reviewed release.
|
|
12
|
+
|
|
13
|
+
AG Grid Enterprise features are licensed by AG Grid's own per-developer
|
|
14
|
+
license (``AG_GRID_LICENSE_KEY`` env var at compile time in each app) —
|
|
15
|
+
entirely unrelated to Reflex.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from sp_reflex_components.ag_grid import (
|
|
19
|
+
AG_GRID_LICENSE_KEY_ENV,
|
|
20
|
+
AG_GRID_VERSION,
|
|
21
|
+
AgGrid,
|
|
22
|
+
grid,
|
|
23
|
+
)
|
|
24
|
+
from sp_reflex_components.react_flow import (
|
|
25
|
+
XYFLOW_VERSION,
|
|
26
|
+
Background,
|
|
27
|
+
Controls,
|
|
28
|
+
Edge,
|
|
29
|
+
Flow,
|
|
30
|
+
Node,
|
|
31
|
+
XYPosition,
|
|
32
|
+
flow,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"AG_GRID_LICENSE_KEY_ENV",
|
|
37
|
+
"AG_GRID_VERSION",
|
|
38
|
+
"XYFLOW_VERSION",
|
|
39
|
+
"AgGrid",
|
|
40
|
+
"Background",
|
|
41
|
+
"Controls",
|
|
42
|
+
"Edge",
|
|
43
|
+
"Flow",
|
|
44
|
+
"Node",
|
|
45
|
+
"XYPosition",
|
|
46
|
+
"flow",
|
|
47
|
+
"grid",
|
|
48
|
+
]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""AG Grid wrapper over ag-grid-react — no reflex-enterprise required.
|
|
2
|
+
|
|
3
|
+
Wraps the same npm packages reflex-enterprise wrapped, scoped to the surface
|
|
4
|
+
consuming apps actually use (see package scope doctrine in ``__init__``).
|
|
5
|
+
AG Grid Enterprise features are licensed by AG_GRID_LICENSE_KEY (AG Grid's
|
|
6
|
+
own per-developer product license, independent of Reflex).
|
|
7
|
+
|
|
8
|
+
v34 theming: the app uses legacy CSS themes (`ag-theme-alpine` class), so the
|
|
9
|
+
grid opts back into legacy mode globally via `provideGlobalGridOptions`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any, ClassVar, TypedDict
|
|
16
|
+
|
|
17
|
+
import reflex as rx
|
|
18
|
+
from reflex.vars.base import Var
|
|
19
|
+
from reflex.vars.object import ObjectVar
|
|
20
|
+
|
|
21
|
+
# Exact pin; bump = deliberate release w/ changelog read (scope doctrine).
|
|
22
|
+
# 36.1.0 chosen 2026-08-07 (v35/v36 upgrade guides: no removals in our surface;
|
|
23
|
+
# legacy theming still supported-but-deprecated — next major may remove it).
|
|
24
|
+
AG_GRID_VERSION = "36.1.0"
|
|
25
|
+
_REACT_PKG = f"ag-grid-react@{AG_GRID_VERSION}"
|
|
26
|
+
_COMMUNITY_PKG = "ag-grid-community"
|
|
27
|
+
_ENTERPRISE_PKG = "ag-grid-enterprise"
|
|
28
|
+
|
|
29
|
+
AG_GRID_LICENSE_KEY_ENV = "AG_GRID_LICENSE_KEY"
|
|
30
|
+
|
|
31
|
+
# Adapter passes theme="alpine" only (§V64 scope); unknown values fall back.
|
|
32
|
+
_THEME_CLASSES = {"alpine": "ag-theme-alpine"}
|
|
33
|
+
_DEFAULT_THEME = "alpine"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CellEventSpec(TypedDict):
|
|
37
|
+
"""Payload for cell click events (shape consumed by existing states)."""
|
|
38
|
+
|
|
39
|
+
type: str
|
|
40
|
+
data: dict
|
|
41
|
+
value: Any
|
|
42
|
+
colDef: dict
|
|
43
|
+
rowIndex: int
|
|
44
|
+
rowPinned: str | None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RowEventSpec(TypedDict):
|
|
48
|
+
"""Payload for row events (shape consumed by existing states)."""
|
|
49
|
+
|
|
50
|
+
type: str
|
|
51
|
+
data: dict
|
|
52
|
+
rowIndex: int
|
|
53
|
+
rowPinned: str | None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CellValueChangedEventSpec(TypedDict):
|
|
57
|
+
"""Payload for cell value edits (shape consumed by existing states)."""
|
|
58
|
+
|
|
59
|
+
rowIndex: int
|
|
60
|
+
field: str
|
|
61
|
+
newValue: Any
|
|
62
|
+
node_id: str
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _on_selection_changed_spec(
|
|
66
|
+
event: ObjectVar[dict],
|
|
67
|
+
) -> tuple[Var[list[dict]], Var[str], Var[str]]:
|
|
68
|
+
return (
|
|
69
|
+
Var(f"{event}.api.getSelectedRows()").to(list[dict]),
|
|
70
|
+
event.source.to(str),
|
|
71
|
+
event.type.to(str),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _on_cell_value_changed_spec(
|
|
76
|
+
event: ObjectVar[dict],
|
|
77
|
+
) -> tuple[Var[CellValueChangedEventSpec]]:
|
|
78
|
+
return (
|
|
79
|
+
Var.create(
|
|
80
|
+
{
|
|
81
|
+
"rowIndex": event.rowIndex,
|
|
82
|
+
# Nested attribute access on an untyped ObjectVar raises
|
|
83
|
+
# UntypedVarError at compile — use explicit JS expressions.
|
|
84
|
+
"field": Var(f"{event}.colDef.field").to(str),
|
|
85
|
+
"newValue": event.newValue,
|
|
86
|
+
"node_id": Var(f"{event}.node.id").to(str),
|
|
87
|
+
}
|
|
88
|
+
).to(CellValueChangedEventSpec),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _on_cell_event_spec(event: ObjectVar[dict]) -> tuple[Var[CellEventSpec]]:
|
|
93
|
+
return (
|
|
94
|
+
Var.create(
|
|
95
|
+
{
|
|
96
|
+
"type": event.type,
|
|
97
|
+
"data": event.data,
|
|
98
|
+
"value": event.value,
|
|
99
|
+
"colDef": event.colDef,
|
|
100
|
+
"rowIndex": event.rowIndex,
|
|
101
|
+
"rowPinned": event.rowPinned,
|
|
102
|
+
}
|
|
103
|
+
).to(CellEventSpec),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _on_row_event_spec(event: ObjectVar[dict]) -> tuple[Var[RowEventSpec]]:
|
|
108
|
+
return (
|
|
109
|
+
Var.create(
|
|
110
|
+
{
|
|
111
|
+
"type": event.type,
|
|
112
|
+
"data": event.data,
|
|
113
|
+
"rowIndex": event.rowIndex,
|
|
114
|
+
"rowPinned": event.rowPinned,
|
|
115
|
+
}
|
|
116
|
+
).to(RowEventSpec),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class AgGrid(rx.NoSSRComponent):
|
|
121
|
+
"""AgGridReact scoped to the consuming apps' adapter surface."""
|
|
122
|
+
|
|
123
|
+
library = _REACT_PKG
|
|
124
|
+
tag = "AgGridReact"
|
|
125
|
+
|
|
126
|
+
# Reflex's component idiom uses a class-attr list here (same upstream).
|
|
127
|
+
lib_dependencies: list[str] = [ # noqa: RUF012
|
|
128
|
+
f"{_COMMUNITY_PKG}@{AG_GRID_VERSION}",
|
|
129
|
+
f"{_ENTERPRISE_PKG}@{AG_GRID_VERSION}",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# AG Grid's own DOM id prop is gridId; `id` collides with the React DOM prop.
|
|
133
|
+
_rename_props: ClassVar[dict[str, str]] = {"id": "gridId"}
|
|
134
|
+
|
|
135
|
+
# --- props (adapter surface, data_grid.py grid_props) ---
|
|
136
|
+
row_data: Var[list[dict]]
|
|
137
|
+
column_defs: Var[list[dict]]
|
|
138
|
+
default_col_def: Var[dict]
|
|
139
|
+
animate_rows: Var[bool]
|
|
140
|
+
header_height: Var[int]
|
|
141
|
+
row_height: Var[int]
|
|
142
|
+
ensure_dom_order: Var[bool]
|
|
143
|
+
enable_cell_text_selection: Var[bool]
|
|
144
|
+
overlay_no_rows_template: Var[str]
|
|
145
|
+
pagination: Var[bool]
|
|
146
|
+
suppress_csv_export: Var[bool]
|
|
147
|
+
cell_selection: Var[bool]
|
|
148
|
+
suppress_clipboard_paste: Var[bool]
|
|
149
|
+
single_click_edit: Var[bool]
|
|
150
|
+
# Correct AG Grid option name (the enterprise wrapper emitted the invalid
|
|
151
|
+
# `stopEditWhenCellLosesFocus`, which AG Grid rejects).
|
|
152
|
+
stop_editing_when_cells_lose_focus: Var[bool]
|
|
153
|
+
dom_layout: Var[str]
|
|
154
|
+
quick_filter_text: Var[str]
|
|
155
|
+
# v35 added noMatchingRows/exporting overlays; adapter suppresses them for
|
|
156
|
+
# v34-parity (custom no-rows overlay remains the only overlay — §V66).
|
|
157
|
+
suppress_overlays: Var[list[str]]
|
|
158
|
+
get_row_id: Var[Any]
|
|
159
|
+
row_selection: Var[dict]
|
|
160
|
+
|
|
161
|
+
# --- events (adapter surface) ---
|
|
162
|
+
on_selection_changed: rx.EventHandler[_on_selection_changed_spec]
|
|
163
|
+
on_cell_value_changed: rx.EventHandler[_on_cell_value_changed_spec]
|
|
164
|
+
on_cell_clicked: rx.EventHandler[_on_cell_event_spec]
|
|
165
|
+
on_row_double_clicked: rx.EventHandler[_on_row_event_spec]
|
|
166
|
+
|
|
167
|
+
def add_imports(self) -> dict[str, list[str]]:
|
|
168
|
+
"""Modules, license manager, and legacy theme CSS."""
|
|
169
|
+
return {
|
|
170
|
+
"": [
|
|
171
|
+
f"{_COMMUNITY_PKG}/styles/ag-grid.css",
|
|
172
|
+
f"{_COMMUNITY_PKG}/styles/ag-theme-alpine.css",
|
|
173
|
+
],
|
|
174
|
+
_COMMUNITY_PKG: [
|
|
175
|
+
"ModuleRegistry",
|
|
176
|
+
"AllCommunityModule",
|
|
177
|
+
"provideGlobalGridOptions",
|
|
178
|
+
],
|
|
179
|
+
_ENTERPRISE_PKG: [
|
|
180
|
+
"ClipboardModule",
|
|
181
|
+
"CellSelectionModule",
|
|
182
|
+
"LicenseManager",
|
|
183
|
+
],
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
def add_custom_code(self) -> list[str]:
|
|
187
|
+
"""License key + module registration + legacy theming."""
|
|
188
|
+
license_key = os.getenv(AG_GRID_LICENSE_KEY_ENV)
|
|
189
|
+
license_arg = f"'{license_key}'" if license_key else "null"
|
|
190
|
+
return [
|
|
191
|
+
f"LicenseManager.setLicenseKey({license_arg});",
|
|
192
|
+
(
|
|
193
|
+
"ModuleRegistry.registerModules("
|
|
194
|
+
"[AllCommunityModule, ClipboardModule, CellSelectionModule]);"
|
|
195
|
+
),
|
|
196
|
+
'provideGlobalGridOptions({"theme": "legacy"});',
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _camelize_keys(value: Any) -> Any:
|
|
201
|
+
"""Recursively camelize dict keys in plain-Python grid option structures.
|
|
202
|
+
|
|
203
|
+
AG Grid ignores snake_case option names (headerName, cellDataType, …), and
|
|
204
|
+
the adapter builds column defs with snake_case keys. Vars and non-container
|
|
205
|
+
values (e.g. FunctionStringVar formatters) pass through untouched.
|
|
206
|
+
"""
|
|
207
|
+
from reflex.utils.format import to_camel_case
|
|
208
|
+
|
|
209
|
+
if isinstance(value, dict):
|
|
210
|
+
return {to_camel_case(key): _camelize_keys(val) for key, val in value.items()}
|
|
211
|
+
if isinstance(value, (list, tuple)):
|
|
212
|
+
return [_camelize_keys(item) for item in value]
|
|
213
|
+
return value
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def grid(*, theme: str = _DEFAULT_THEME, **props: Any) -> rx.Component:
|
|
217
|
+
"""AG Grid inside a legacy-theme container filling its parent.
|
|
218
|
+
|
|
219
|
+
Drop-in replacement for reflex-enterprise's `ag_grid.root`. Nested
|
|
220
|
+
column_defs/default_col_def keys are camelized here (adapter-scoped
|
|
221
|
+
normalization — AgGrid itself stays a pure passthrough).
|
|
222
|
+
"""
|
|
223
|
+
for key in ("column_defs", "default_col_def"):
|
|
224
|
+
if key in props and not isinstance(props[key], Var):
|
|
225
|
+
props[key] = _camelize_keys(props[key])
|
|
226
|
+
theme_class = _THEME_CLASSES.get(theme, _THEME_CLASSES[_DEFAULT_THEME])
|
|
227
|
+
return rx.el.div(
|
|
228
|
+
AgGrid.create(**props),
|
|
229
|
+
class_name=theme_class,
|
|
230
|
+
style={"height": "100%", "width": "100%"},
|
|
231
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""React Flow wrapper over @xyflow/react — no reflex-enterprise required.
|
|
2
|
+
|
|
3
|
+
Scoped to the surface consuming apps use: the ReactFlow canvas with static
|
|
4
|
+
(controlled) nodes/edges, Controls, and a Background pattern.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, TypedDict
|
|
10
|
+
|
|
11
|
+
import reflex as rx
|
|
12
|
+
from reflex.vars.base import Var
|
|
13
|
+
|
|
14
|
+
# Exact pin; bump = deliberate release. 12.11.2 = latest 12.x (2026-08-07),
|
|
15
|
+
# same major reflex-enterprise wrapped (@xyflow/react@12.8.4).
|
|
16
|
+
XYFLOW_VERSION = "12.11.2"
|
|
17
|
+
_LIBRARY = f"@xyflow/react@{XYFLOW_VERSION}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class XYPosition(TypedDict):
|
|
21
|
+
"""Canvas coordinates."""
|
|
22
|
+
|
|
23
|
+
x: float
|
|
24
|
+
y: float
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Node(TypedDict, total=False):
|
|
28
|
+
"""React Flow node — fields our layout payload emits."""
|
|
29
|
+
|
|
30
|
+
id: str
|
|
31
|
+
position: XYPosition
|
|
32
|
+
data: dict
|
|
33
|
+
style: dict
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Edge(TypedDict, total=False):
|
|
37
|
+
"""React Flow edge — fields our layout payload emits."""
|
|
38
|
+
|
|
39
|
+
id: str
|
|
40
|
+
source: str
|
|
41
|
+
target: str
|
|
42
|
+
label: str
|
|
43
|
+
type: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _FlowBase(rx.NoSSRComponent):
|
|
47
|
+
library = _LIBRARY
|
|
48
|
+
|
|
49
|
+
def add_imports(self) -> dict[str, list[str]]:
|
|
50
|
+
return {"": ["@xyflow/react/dist/style.css"]}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Flow(_FlowBase):
|
|
54
|
+
"""ReactFlow canvas scoped to the consuming apps' adapter surface."""
|
|
55
|
+
|
|
56
|
+
tag = "ReactFlow"
|
|
57
|
+
is_default = False
|
|
58
|
+
|
|
59
|
+
nodes: Var[list[Node]]
|
|
60
|
+
edges: Var[list[Edge]]
|
|
61
|
+
fit_view: Var[bool]
|
|
62
|
+
nodes_draggable: Var[bool]
|
|
63
|
+
nodes_connectable: Var[bool]
|
|
64
|
+
edges_reconnectable: Var[bool]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Controls(_FlowBase):
|
|
68
|
+
"""Zoom/fit controls."""
|
|
69
|
+
|
|
70
|
+
tag = "Controls"
|
|
71
|
+
is_default = False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Background(_FlowBase):
|
|
75
|
+
"""Canvas background pattern."""
|
|
76
|
+
|
|
77
|
+
tag = "Background"
|
|
78
|
+
is_default = False
|
|
79
|
+
|
|
80
|
+
variant: Var[str]
|
|
81
|
+
gap: Var[int]
|
|
82
|
+
size: Var[int]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _FlowNamespace:
|
|
86
|
+
"""Callable namespace mirroring the adapter's `flow(...)` usage."""
|
|
87
|
+
|
|
88
|
+
controls = Controls.create
|
|
89
|
+
background = Background.create
|
|
90
|
+
|
|
91
|
+
def __call__(self, *children: Any, **props: Any) -> rx.Component:
|
|
92
|
+
return Flow.create(*children, **props)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
flow = _FlowNamespace()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Standalone contract tests for the shared wrappers.
|
|
2
|
+
|
|
3
|
+
App-specific adapter/wrapper prop-surface EQUALITY tests live in each
|
|
4
|
+
consuming app (they need that app's adapter); these cover everything the
|
|
5
|
+
package can prove on its own.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sp_reflex_components as spc
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_ag_grid_pins_exact_and_consistent() -> None:
|
|
14
|
+
version = spc.AG_GRID_VERSION
|
|
15
|
+
assert version.count(".") == 2 and all(p.isdigit() for p in version.split("."))
|
|
16
|
+
assert spc.AgGrid.library == f"ag-grid-react@{version}"
|
|
17
|
+
assert set(spc.AgGrid().lib_dependencies) == {
|
|
18
|
+
f"ag-grid-community@{version}",
|
|
19
|
+
f"ag-grid-enterprise@{version}",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_ag_grid_modules_license_and_css() -> None:
|
|
24
|
+
component = spc.AgGrid.create()
|
|
25
|
+
|
|
26
|
+
code = "\n".join(component.add_custom_code())
|
|
27
|
+
assert "LicenseManager.setLicenseKey(" in code
|
|
28
|
+
assert "ModuleRegistry.registerModules(" in code
|
|
29
|
+
for module in ("AllCommunityModule", "ClipboardModule", "CellSelectionModule"):
|
|
30
|
+
assert module in code
|
|
31
|
+
assert '"theme": "legacy"' in code
|
|
32
|
+
|
|
33
|
+
imports = component.add_imports()
|
|
34
|
+
assert "ag-grid-community/styles/ag-grid.css" in imports[""]
|
|
35
|
+
assert "ag-grid-community/styles/ag-theme-alpine.css" in imports[""]
|
|
36
|
+
assert "LicenseManager" in imports["ag-grid-enterprise"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_ag_grid_license_key_from_env(monkeypatch) -> None:
|
|
40
|
+
monkeypatch.setenv(spc.AG_GRID_LICENSE_KEY_ENV, "test-key-123")
|
|
41
|
+
assert "LicenseManager.setLicenseKey('test-key-123');" in spc.AgGrid.create().add_custom_code()
|
|
42
|
+
|
|
43
|
+
monkeypatch.delenv(spc.AG_GRID_LICENSE_KEY_ENV)
|
|
44
|
+
assert "LicenseManager.setLicenseKey(null);" in spc.AgGrid.create().add_custom_code()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_ag_grid_emits_correct_stop_editing_prop() -> None:
|
|
48
|
+
rendered = str(spc.AgGrid.create(stop_editing_when_cells_lose_focus=True).render())
|
|
49
|
+
assert "stopEditingWhenCellsLoseFocus" in rendered
|
|
50
|
+
assert "stopEditWhenCellLosesFocus" not in rendered
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_grid_helper_camelizes_nested_option_keys() -> None:
|
|
54
|
+
rendered = str(
|
|
55
|
+
spc.grid(
|
|
56
|
+
row_data=[],
|
|
57
|
+
column_defs=[
|
|
58
|
+
{
|
|
59
|
+
"field": "name",
|
|
60
|
+
"header_name": "Name",
|
|
61
|
+
"cell_data_type": "text",
|
|
62
|
+
"cell_editor_params": {"values": ["a", "b"]},
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
default_col_def={"min_width": 72, "suppress_header_menu_button": True},
|
|
66
|
+
).render()
|
|
67
|
+
)
|
|
68
|
+
for camel in (
|
|
69
|
+
"headerName",
|
|
70
|
+
"cellDataType",
|
|
71
|
+
"cellEditorParams",
|
|
72
|
+
"minWidth",
|
|
73
|
+
"suppressHeaderMenuButton",
|
|
74
|
+
):
|
|
75
|
+
assert camel in rendered, f"missing camelized key {camel}"
|
|
76
|
+
for snake in ("header_name", "cell_data_type", "min_width"):
|
|
77
|
+
assert snake not in rendered, f"unconverted key {snake} leaked through"
|
|
78
|
+
assert "ag-theme-alpine" in rendered
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_react_flow_pin_and_style_css() -> None:
|
|
82
|
+
version = spc.XYFLOW_VERSION
|
|
83
|
+
assert version.count(".") == 2 and all(p.isdigit() for p in version.split("."))
|
|
84
|
+
assert spc.Flow.library == f"@xyflow/react@{version}"
|
|
85
|
+
assert "@xyflow/react/dist/style.css" in spc.Flow.create().add_imports()[""]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_flow_namespace_matches_enterprise_call_shape() -> None:
|
|
89
|
+
component = spc.flow(
|
|
90
|
+
spc.flow.controls(),
|
|
91
|
+
spc.flow.background(variant="dots", gap=18, size=1),
|
|
92
|
+
id="canvas",
|
|
93
|
+
nodes=[],
|
|
94
|
+
edges=[],
|
|
95
|
+
fit_view=True,
|
|
96
|
+
nodes_draggable=False,
|
|
97
|
+
nodes_connectable=False,
|
|
98
|
+
edges_reconnectable=False,
|
|
99
|
+
)
|
|
100
|
+
rendered = str(component.render())
|
|
101
|
+
assert "ReactFlow" in rendered
|
|
102
|
+
assert "Controls" in rendered
|
|
103
|
+
assert "Background" in rendered
|