reflex 0.8.11__py3-none-any.whl → 0.8.12__py3-none-any.whl
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.
Potentially problematic release.
This version of reflex might be problematic. Click here for more details.
- reflex/.templates/web/utils/state.js +56 -21
- reflex/app.py +17 -10
- reflex/app_mixins/lifespan.py +11 -0
- reflex/compiler/compiler.py +4 -1
- reflex/compiler/templates.py +1 -1
- reflex/components/core/upload.py +53 -14
- reflex/components/core/upload.pyi +8 -0
- reflex/components/lucide/icon.py +2 -1
- reflex/components/lucide/icon.pyi +2 -1
- reflex/constants/installer.py +4 -4
- reflex/event.py +1 -1
- reflex/istate/manager.py +2 -2
- reflex/istate/proxy.py +36 -36
- reflex/reflex.py +18 -0
- reflex/state.py +2 -2
- reflex/style.py +4 -2
- reflex/testing.py +14 -0
- reflex/utils/build.py +67 -53
- reflex/utils/console.py +17 -1
- reflex/utils/exec.py +4 -2
- reflex/utils/export.py +4 -1
- reflex/utils/token_manager.py +10 -0
- reflex/utils/types.py +2 -0
- reflex/vars/base.py +2 -2
- reflex/vars/object.py +6 -21
- {reflex-0.8.11.dist-info → reflex-0.8.12.dist-info}/METADATA +1 -1
- {reflex-0.8.11.dist-info → reflex-0.8.12.dist-info}/RECORD +30 -30
- {reflex-0.8.11.dist-info → reflex-0.8.12.dist-info}/WHEEL +0 -0
- {reflex-0.8.11.dist-info → reflex-0.8.12.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.11.dist-info → reflex-0.8.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -478,8 +478,8 @@ export const queueEvents = async (
|
|
|
478
478
|
* @param params The params object from React Router
|
|
479
479
|
*/
|
|
480
480
|
export const processEvent = async (socket, navigate, params) => {
|
|
481
|
-
// Only proceed if the socket is up
|
|
482
|
-
if (!socket &&
|
|
481
|
+
// Only proceed if the socket is up or no event in the queue uses state, otherwise we throw the event into the void
|
|
482
|
+
if (isStateful() && !(socket && socket.connected)) {
|
|
483
483
|
return;
|
|
484
484
|
}
|
|
485
485
|
|
|
@@ -531,6 +531,7 @@ export const connect = async (
|
|
|
531
531
|
) => {
|
|
532
532
|
// Get backend URL object from the endpoint.
|
|
533
533
|
const endpoint = getBackendURL(EVENTURL);
|
|
534
|
+
const on_hydrated_queue = [];
|
|
534
535
|
|
|
535
536
|
// Create the socket.
|
|
536
537
|
socket.current = io(endpoint.href, {
|
|
@@ -552,7 +553,17 @@ export const connect = async (
|
|
|
552
553
|
|
|
553
554
|
function checkVisibility() {
|
|
554
555
|
if (document.visibilityState === "visible") {
|
|
555
|
-
if (!socket.current
|
|
556
|
+
if (!socket.current) {
|
|
557
|
+
connect(
|
|
558
|
+
socket,
|
|
559
|
+
dispatch,
|
|
560
|
+
transports,
|
|
561
|
+
setConnectErrors,
|
|
562
|
+
client_storage,
|
|
563
|
+
navigate,
|
|
564
|
+
params,
|
|
565
|
+
);
|
|
566
|
+
} else if (!socket.current.connected) {
|
|
556
567
|
console.log("Socket is disconnected, attempting to reconnect ");
|
|
557
568
|
socket.current.connect();
|
|
558
569
|
} else {
|
|
@@ -576,11 +587,15 @@ export const connect = async (
|
|
|
576
587
|
};
|
|
577
588
|
|
|
578
589
|
// Once the socket is open, hydrate the page.
|
|
579
|
-
socket.current.on("connect", () => {
|
|
590
|
+
socket.current.on("connect", async () => {
|
|
580
591
|
setConnectErrors([]);
|
|
581
592
|
window.addEventListener("pagehide", pagehideHandler);
|
|
582
593
|
window.addEventListener("beforeunload", disconnectTrigger);
|
|
583
594
|
window.addEventListener("unload", disconnectTrigger);
|
|
595
|
+
// Drain any initial events from the queue.
|
|
596
|
+
while (event_queue.length > 0 && !event_processing) {
|
|
597
|
+
await processEvent(socket.current, navigate, () => params.current);
|
|
598
|
+
}
|
|
584
599
|
});
|
|
585
600
|
|
|
586
601
|
socket.current.on("connect_error", (error) => {
|
|
@@ -589,6 +604,7 @@ export const connect = async (
|
|
|
589
604
|
|
|
590
605
|
// When the socket disconnects reset the event_processing flag
|
|
591
606
|
socket.current.on("disconnect", () => {
|
|
607
|
+
socket.current = null; // allow reconnect to occur automatically
|
|
592
608
|
event_processing = false;
|
|
593
609
|
window.removeEventListener("unload", disconnectTrigger);
|
|
594
610
|
window.removeEventListener("beforeunload", disconnectTrigger);
|
|
@@ -599,6 +615,14 @@ export const connect = async (
|
|
|
599
615
|
socket.current.on("event", async (update) => {
|
|
600
616
|
for (const substate in update.delta) {
|
|
601
617
|
dispatch[substate](update.delta[substate]);
|
|
618
|
+
// handle events waiting for `is_hydrated`
|
|
619
|
+
if (
|
|
620
|
+
substate === state_name &&
|
|
621
|
+
update.delta[substate]?.is_hydrated_rx_state_
|
|
622
|
+
) {
|
|
623
|
+
queueEvents(on_hydrated_queue, socket, false, navigate, params);
|
|
624
|
+
on_hydrated_queue.length = 0;
|
|
625
|
+
}
|
|
602
626
|
}
|
|
603
627
|
applyClientStorageDelta(client_storage, update.delta);
|
|
604
628
|
event_processing = !update.final;
|
|
@@ -608,7 +632,8 @@ export const connect = async (
|
|
|
608
632
|
});
|
|
609
633
|
socket.current.on("reload", async (event) => {
|
|
610
634
|
event_processing = false;
|
|
611
|
-
|
|
635
|
+
on_hydrated_queue.push(event);
|
|
636
|
+
queueEvents(initialEvents(), socket, true, navigate, params);
|
|
612
637
|
});
|
|
613
638
|
socket.current.on("new_token", async (new_token) => {
|
|
614
639
|
token = new_token;
|
|
@@ -770,10 +795,32 @@ export const useEventLoop = (
|
|
|
770
795
|
}
|
|
771
796
|
}, [paramsR]);
|
|
772
797
|
|
|
798
|
+
const ensureSocketConnected = useCallback(async () => {
|
|
799
|
+
// only use websockets if state is present and backend is not disabled (reflex cloud).
|
|
800
|
+
if (
|
|
801
|
+
Object.keys(initialState).length > 1 &&
|
|
802
|
+
!isBackendDisabled() &&
|
|
803
|
+
!socket.current
|
|
804
|
+
) {
|
|
805
|
+
// Initialize the websocket connection.
|
|
806
|
+
await connect(
|
|
807
|
+
socket,
|
|
808
|
+
dispatch,
|
|
809
|
+
["websocket"],
|
|
810
|
+
setConnectErrors,
|
|
811
|
+
client_storage,
|
|
812
|
+
navigate,
|
|
813
|
+
() => params.current,
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
}, [socket, dispatch, setConnectErrors, client_storage, navigate, params]);
|
|
817
|
+
|
|
773
818
|
// Function to add new events to the event queue.
|
|
774
819
|
const addEvents = useCallback((events, args, event_actions) => {
|
|
775
820
|
const _events = events.filter((e) => e !== undefined && e !== null);
|
|
776
821
|
|
|
822
|
+
ensureSocketConnected();
|
|
823
|
+
|
|
777
824
|
if (!(args instanceof Array)) {
|
|
778
825
|
args = [args];
|
|
779
826
|
}
|
|
@@ -866,21 +913,8 @@ export const useEventLoop = (
|
|
|
866
913
|
|
|
867
914
|
// Handle socket connect/disconnect.
|
|
868
915
|
useEffect(() => {
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
// Initialize the websocket connection.
|
|
872
|
-
if (!socket.current) {
|
|
873
|
-
connect(
|
|
874
|
-
socket,
|
|
875
|
-
dispatch,
|
|
876
|
-
["websocket"],
|
|
877
|
-
setConnectErrors,
|
|
878
|
-
client_storage,
|
|
879
|
-
navigate,
|
|
880
|
-
() => params.current,
|
|
881
|
-
);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
916
|
+
// Initialize the websocket connection.
|
|
917
|
+
ensureSocketConnected();
|
|
884
918
|
|
|
885
919
|
// Cleanup function.
|
|
886
920
|
return () => {
|
|
@@ -893,12 +927,13 @@ export const useEventLoop = (
|
|
|
893
927
|
// Main event loop.
|
|
894
928
|
useEffect(() => {
|
|
895
929
|
// Skip if the backend is disabled
|
|
896
|
-
if (isBackendDisabled()) {
|
|
930
|
+
if (isBackendDisabled() || !socket.current || !socket.current.connected) {
|
|
897
931
|
return;
|
|
898
932
|
}
|
|
899
933
|
(async () => {
|
|
900
934
|
// Process all outstanding events.
|
|
901
935
|
while (event_queue.length > 0 && !event_processing) {
|
|
936
|
+
await ensureSocketConnected();
|
|
902
937
|
await processEvent(socket.current, navigate, () => params.current);
|
|
903
938
|
}
|
|
904
939
|
})();
|
reflex/app.py
CHANGED
|
@@ -97,6 +97,7 @@ from reflex.state import (
|
|
|
97
97
|
State,
|
|
98
98
|
StateManager,
|
|
99
99
|
StateUpdate,
|
|
100
|
+
_split_substate_key,
|
|
100
101
|
_substate_key,
|
|
101
102
|
all_base_state_classes,
|
|
102
103
|
code_uses_state_contexts,
|
|
@@ -1559,7 +1560,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1559
1560
|
state._clean()
|
|
1560
1561
|
await self.event_namespace.emit_update(
|
|
1561
1562
|
update=StateUpdate(delta=delta),
|
|
1562
|
-
|
|
1563
|
+
token=token,
|
|
1563
1564
|
)
|
|
1564
1565
|
|
|
1565
1566
|
def _process_background(
|
|
@@ -1599,7 +1600,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1599
1600
|
# Send the update to the client.
|
|
1600
1601
|
await self.event_namespace.emit_update(
|
|
1601
1602
|
update=update,
|
|
1602
|
-
|
|
1603
|
+
token=event.token,
|
|
1603
1604
|
)
|
|
1604
1605
|
|
|
1605
1606
|
task = asyncio.create_task(
|
|
@@ -2061,20 +2062,19 @@ class EventNamespace(AsyncNamespace):
|
|
|
2061
2062
|
and console.error(f"Token cleanup error: {t.exception()}")
|
|
2062
2063
|
)
|
|
2063
2064
|
|
|
2064
|
-
async def emit_update(self, update: StateUpdate,
|
|
2065
|
+
async def emit_update(self, update: StateUpdate, token: str) -> None:
|
|
2065
2066
|
"""Emit an update to the client.
|
|
2066
2067
|
|
|
2067
2068
|
Args:
|
|
2068
2069
|
update: The state update to send.
|
|
2069
|
-
|
|
2070
|
+
token: The client token (tab) associated with the event.
|
|
2070
2071
|
"""
|
|
2071
|
-
|
|
2072
|
+
client_token, _ = _split_substate_key(token)
|
|
2073
|
+
sid = self.token_to_sid.get(client_token)
|
|
2074
|
+
if sid is None:
|
|
2072
2075
|
# If the sid is None, we are not connected to a client. Prevent sending
|
|
2073
2076
|
# updates to all clients.
|
|
2074
|
-
|
|
2075
|
-
token = self.sid_to_token.get(sid)
|
|
2076
|
-
if token is None:
|
|
2077
|
-
console.warn(f"Attempting to send delta to disconnected websocket {sid}")
|
|
2077
|
+
console.warn(f"Attempting to send delta to disconnected client {token!r}")
|
|
2078
2078
|
return
|
|
2079
2079
|
# Creating a task prevents the update from being blocked behind other coroutines.
|
|
2080
2080
|
await asyncio.create_task(
|
|
@@ -2165,7 +2165,7 @@ class EventNamespace(AsyncNamespace):
|
|
|
2165
2165
|
# Process the events.
|
|
2166
2166
|
async for update in updates_gen:
|
|
2167
2167
|
# Emit the update from processing the event.
|
|
2168
|
-
await self.emit_update(update=update,
|
|
2168
|
+
await self.emit_update(update=update, token=event.token)
|
|
2169
2169
|
|
|
2170
2170
|
async def on_ping(self, sid: str):
|
|
2171
2171
|
"""Event for testing the API endpoint.
|
|
@@ -2189,3 +2189,10 @@ class EventNamespace(AsyncNamespace):
|
|
|
2189
2189
|
if new_token:
|
|
2190
2190
|
# Duplicate detected, emit new token to client
|
|
2191
2191
|
await self.emit("new_token", new_token, to=sid)
|
|
2192
|
+
|
|
2193
|
+
# Update client state to apply new sid/token for running background tasks.
|
|
2194
|
+
async with self.app.modify_state(
|
|
2195
|
+
_substate_key(new_token or token, self.app.state_manager.state)
|
|
2196
|
+
) as state:
|
|
2197
|
+
state.router_data[constants.RouteVar.SESSION_ID] = sid
|
|
2198
|
+
state.router = RouterData.from_router_data(state.router_data)
|
reflex/app_mixins/lifespan.py
CHANGED
|
@@ -60,6 +60,17 @@ class LifespanMixin(AppMixin):
|
|
|
60
60
|
for task in running_tasks:
|
|
61
61
|
console.debug(f"Canceling lifespan task: {task}")
|
|
62
62
|
task.cancel(msg="lifespan_cleanup")
|
|
63
|
+
# Disassociate sid / token pairings so they can be reconnected properly.
|
|
64
|
+
try:
|
|
65
|
+
event_namespace = self.event_namespace # pyright: ignore[reportAttributeAccessIssue]
|
|
66
|
+
except AttributeError:
|
|
67
|
+
pass
|
|
68
|
+
else:
|
|
69
|
+
try:
|
|
70
|
+
if event_namespace:
|
|
71
|
+
await event_namespace._token_manager.disconnect_all()
|
|
72
|
+
except Exception as e:
|
|
73
|
+
console.error(f"Error during lifespan cleanup: {e}")
|
|
63
74
|
|
|
64
75
|
def register_lifespan_task(self, task: Callable | asyncio.Task, **task_kwargs):
|
|
65
76
|
"""Register a task to run during the lifespan of the app.
|
reflex/compiler/compiler.py
CHANGED
|
@@ -621,7 +621,10 @@ def purge_web_pages_dir():
|
|
|
621
621
|
return
|
|
622
622
|
|
|
623
623
|
# Empty out the web pages directory.
|
|
624
|
-
utils.empty_dir(
|
|
624
|
+
utils.empty_dir(
|
|
625
|
+
get_web_dir() / constants.Dirs.PAGES,
|
|
626
|
+
keep_files=["routes.js", "entry.client.js"],
|
|
627
|
+
)
|
|
625
628
|
|
|
626
629
|
|
|
627
630
|
if TYPE_CHECKING:
|
reflex/compiler/templates.py
CHANGED
|
@@ -86,7 +86,7 @@ class _RenderUtils:
|
|
|
86
86
|
children_rendered = "".join(
|
|
87
87
|
[_RenderUtils.render(child) for child in component.get("children", [])]
|
|
88
88
|
)
|
|
89
|
-
return f"{component['iterable_state']}
|
|
89
|
+
return f"Array.prototype.map.call({component['iterable_state']} ?? [],(({component['arg_name']},{component['arg_index']})=>({children_rendered})))"
|
|
90
90
|
|
|
91
91
|
@staticmethod
|
|
92
92
|
def render_match_tag(component: Any) -> str:
|
reflex/components/core/upload.py
CHANGED
|
@@ -17,6 +17,7 @@ from reflex.components.component import (
|
|
|
17
17
|
from reflex.components.core.cond import cond
|
|
18
18
|
from reflex.components.el.elements.forms import Input
|
|
19
19
|
from reflex.components.radix.themes.layout.box import Box
|
|
20
|
+
from reflex.components.sonner.toast import toast
|
|
20
21
|
from reflex.constants import Dirs
|
|
21
22
|
from reflex.constants.compiler import Hooks, Imports
|
|
22
23
|
from reflex.environment import environment
|
|
@@ -36,7 +37,8 @@ from reflex.utils.imports import ImportVar
|
|
|
36
37
|
from reflex.vars import VarData
|
|
37
38
|
from reflex.vars.base import Var, get_unique_variable_name
|
|
38
39
|
from reflex.vars.function import FunctionVar
|
|
39
|
-
from reflex.vars.
|
|
40
|
+
from reflex.vars.object import ObjectVar
|
|
41
|
+
from reflex.vars.sequence import ArrayVar, LiteralStringVar
|
|
40
42
|
|
|
41
43
|
DEFAULT_UPLOAD_ID: str = "default"
|
|
42
44
|
|
|
@@ -178,6 +180,34 @@ def _on_drop_spec(files: Var) -> tuple[Var[Any]]:
|
|
|
178
180
|
return (files,)
|
|
179
181
|
|
|
180
182
|
|
|
183
|
+
def _default_drop_rejected(rejected_files: ArrayVar[list[dict[str, Any]]]) -> EventSpec:
|
|
184
|
+
"""Event handler for showing a toast with rejected file info.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
rejected_files: The files that were rejected.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
An event spec that shows a toast with the rejected file info when triggered.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def _format_rejected_file_record(rf: ObjectVar[dict[str, Any]]) -> str:
|
|
194
|
+
rf = rf.to(ObjectVar, dict[str, dict[str, Any]])
|
|
195
|
+
file = rf["file"].to(ObjectVar, dict[str, Any])
|
|
196
|
+
errors = rf["errors"].to(ArrayVar, list[dict[str, Any]])
|
|
197
|
+
return (
|
|
198
|
+
f"{file['path']}: {errors.foreach(lambda err: err['message']).join(', ')}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return toast.error(
|
|
202
|
+
title="Files not Accepted",
|
|
203
|
+
description=rejected_files.to(ArrayVar)
|
|
204
|
+
.foreach(_format_rejected_file_record)
|
|
205
|
+
.join("\n\n"),
|
|
206
|
+
close_button=True,
|
|
207
|
+
style={"white_space": "pre-line"},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
181
211
|
class UploadFilesProvider(Component):
|
|
182
212
|
"""AppWrap component that provides a dict of selected files by ID via useContext."""
|
|
183
213
|
|
|
@@ -191,6 +221,9 @@ class GhostUpload(Fragment):
|
|
|
191
221
|
# Fired when files are dropped.
|
|
192
222
|
on_drop: EventHandler[_on_drop_spec]
|
|
193
223
|
|
|
224
|
+
# Fired when dropped files do not meet the specified criteria.
|
|
225
|
+
on_drop_rejected: EventHandler[_on_drop_spec]
|
|
226
|
+
|
|
194
227
|
|
|
195
228
|
class Upload(MemoizationLeaf):
|
|
196
229
|
"""A file upload component."""
|
|
@@ -234,6 +267,9 @@ class Upload(MemoizationLeaf):
|
|
|
234
267
|
# Fired when files are dropped.
|
|
235
268
|
on_drop: EventHandler[_on_drop_spec]
|
|
236
269
|
|
|
270
|
+
# Fired when dropped files do not meet the specified criteria.
|
|
271
|
+
on_drop_rejected: EventHandler[_on_drop_spec]
|
|
272
|
+
|
|
237
273
|
# Style rules to apply when actively dragging.
|
|
238
274
|
drag_active_style: Style | None = field(default=None, is_javascript_property=False)
|
|
239
275
|
|
|
@@ -295,6 +331,10 @@ class Upload(MemoizationLeaf):
|
|
|
295
331
|
on_drop[ix] = event
|
|
296
332
|
upload_props["on_drop"] = on_drop
|
|
297
333
|
|
|
334
|
+
if upload_props.get("on_drop_rejected") is None:
|
|
335
|
+
# If on_drop_rejected is not provided, show an error toast.
|
|
336
|
+
upload_props["on_drop_rejected"] = _default_drop_rejected
|
|
337
|
+
|
|
298
338
|
input_props_unique_name = get_unique_variable_name()
|
|
299
339
|
root_props_unique_name = get_unique_variable_name()
|
|
300
340
|
is_drag_active_unique_name = get_unique_variable_name()
|
|
@@ -313,22 +353,22 @@ class Upload(MemoizationLeaf):
|
|
|
313
353
|
),
|
|
314
354
|
)
|
|
315
355
|
|
|
316
|
-
|
|
317
|
-
GhostUpload.create(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
356
|
+
event_triggers = StatefulComponent._get_memoized_event_triggers(
|
|
357
|
+
GhostUpload.create(
|
|
358
|
+
on_drop=upload_props["on_drop"],
|
|
359
|
+
on_drop_rejected=upload_props["on_drop_rejected"],
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
callback_hooks = []
|
|
363
|
+
for trigger_name, (event_var, callback_str) in event_triggers.items():
|
|
364
|
+
upload_props[trigger_name] = event_var
|
|
365
|
+
callback_hooks.append(callback_str)
|
|
321
366
|
|
|
322
367
|
upload_props = {
|
|
323
368
|
format.to_camel_case(key): value for key, value in upload_props.items()
|
|
324
369
|
}
|
|
325
370
|
|
|
326
|
-
use_dropzone_arguments = Var.create(
|
|
327
|
-
{
|
|
328
|
-
"onDrop": event_var,
|
|
329
|
-
**upload_props,
|
|
330
|
-
}
|
|
331
|
-
)
|
|
371
|
+
use_dropzone_arguments = Var.create(upload_props)
|
|
332
372
|
|
|
333
373
|
left_side = (
|
|
334
374
|
"const { "
|
|
@@ -344,11 +384,10 @@ class Upload(MemoizationLeaf):
|
|
|
344
384
|
imports=Imports.EVENTS,
|
|
345
385
|
hooks={Hooks.EVENTS: None},
|
|
346
386
|
),
|
|
347
|
-
event_var._get_all_var_data(),
|
|
348
387
|
use_dropzone_arguments._get_all_var_data(),
|
|
349
388
|
VarData(
|
|
350
389
|
hooks={
|
|
351
|
-
|
|
390
|
+
**dict.fromkeys(callback_hooks, None),
|
|
352
391
|
f"{left_side} = {right_side};": None,
|
|
353
392
|
},
|
|
354
393
|
imports={
|
|
@@ -108,6 +108,7 @@ class GhostUpload(Fragment):
|
|
|
108
108
|
on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
|
|
109
109
|
on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
|
|
110
110
|
on_drop: EventType[()] | EventType[Any] | None = None,
|
|
111
|
+
on_drop_rejected: EventType[()] | EventType[Any] | None = None,
|
|
111
112
|
on_focus: EventType[()] | None = None,
|
|
112
113
|
on_mount: EventType[()] | None = None,
|
|
113
114
|
on_mouse_down: EventType[()] | None = None,
|
|
@@ -127,6 +128,7 @@ class GhostUpload(Fragment):
|
|
|
127
128
|
Args:
|
|
128
129
|
*children: The children of the component.
|
|
129
130
|
on_drop: Fired when files are dropped.
|
|
131
|
+
on_drop_rejected: Fired when dropped files do not meet the specified criteria.
|
|
130
132
|
style: The style of the component.
|
|
131
133
|
key: A unique key for the component.
|
|
132
134
|
id: The id for the component.
|
|
@@ -171,6 +173,7 @@ class Upload(MemoizationLeaf):
|
|
|
171
173
|
on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
|
|
172
174
|
on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
|
|
173
175
|
on_drop: EventType[()] | EventType[Any] | None = None,
|
|
176
|
+
on_drop_rejected: EventType[()] | EventType[Any] | None = None,
|
|
174
177
|
on_focus: EventType[()] | None = None,
|
|
175
178
|
on_mount: EventType[()] | None = None,
|
|
176
179
|
on_mouse_down: EventType[()] | None = None,
|
|
@@ -199,6 +202,7 @@ class Upload(MemoizationLeaf):
|
|
|
199
202
|
no_drag: Whether to disable drag and drop.
|
|
200
203
|
no_keyboard: Whether to disable using the space/enter keys to upload.
|
|
201
204
|
on_drop: Fired when files are dropped.
|
|
205
|
+
on_drop_rejected: Fired when dropped files do not meet the specified criteria.
|
|
202
206
|
drag_active_style: Style rules to apply when actively dragging.
|
|
203
207
|
style: The style of the component.
|
|
204
208
|
key: A unique key for the component.
|
|
@@ -242,6 +246,7 @@ class StyledUpload(Upload):
|
|
|
242
246
|
on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
|
|
243
247
|
on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
|
|
244
248
|
on_drop: EventType[()] | EventType[Any] | None = None,
|
|
249
|
+
on_drop_rejected: EventType[()] | EventType[Any] | None = None,
|
|
245
250
|
on_focus: EventType[()] | None = None,
|
|
246
251
|
on_mount: EventType[()] | None = None,
|
|
247
252
|
on_mouse_down: EventType[()] | None = None,
|
|
@@ -270,6 +275,7 @@ class StyledUpload(Upload):
|
|
|
270
275
|
no_drag: Whether to disable drag and drop.
|
|
271
276
|
no_keyboard: Whether to disable using the space/enter keys to upload.
|
|
272
277
|
on_drop: Fired when files are dropped.
|
|
278
|
+
on_drop_rejected: Fired when dropped files do not meet the specified criteria.
|
|
273
279
|
drag_active_style: Style rules to apply when actively dragging.
|
|
274
280
|
style: The style of the component.
|
|
275
281
|
key: A unique key for the component.
|
|
@@ -314,6 +320,7 @@ class UploadNamespace(ComponentNamespace):
|
|
|
314
320
|
on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
|
|
315
321
|
on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
|
|
316
322
|
on_drop: EventType[()] | EventType[Any] | None = None,
|
|
323
|
+
on_drop_rejected: EventType[()] | EventType[Any] | None = None,
|
|
317
324
|
on_focus: EventType[()] | None = None,
|
|
318
325
|
on_mount: EventType[()] | None = None,
|
|
319
326
|
on_mouse_down: EventType[()] | None = None,
|
|
@@ -342,6 +349,7 @@ class UploadNamespace(ComponentNamespace):
|
|
|
342
349
|
no_drag: Whether to disable drag and drop.
|
|
343
350
|
no_keyboard: Whether to disable using the space/enter keys to upload.
|
|
344
351
|
on_drop: Fired when files are dropped.
|
|
352
|
+
on_drop_rejected: Fired when dropped files do not meet the specified criteria.
|
|
345
353
|
drag_active_style: Style rules to apply when actively dragging.
|
|
346
354
|
style: The style of the component.
|
|
347
355
|
key: A unique key for the component.
|
reflex/components/lucide/icon.py
CHANGED
|
@@ -6,7 +6,7 @@ from reflex.utils.imports import ImportVar
|
|
|
6
6
|
from reflex.vars.base import LiteralVar, Var
|
|
7
7
|
from reflex.vars.sequence import LiteralStringVar, StringVar
|
|
8
8
|
|
|
9
|
-
LUCIDE_LIBRARY = "lucide-react@0.
|
|
9
|
+
LUCIDE_LIBRARY = "lucide-react@0.544.0"
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class LucideIconComponent(Component):
|
|
@@ -674,6 +674,7 @@ LUCIDE_ICON_LIST = [
|
|
|
674
674
|
"eraser",
|
|
675
675
|
"ethernet_port",
|
|
676
676
|
"euro",
|
|
677
|
+
"ev_charger",
|
|
677
678
|
"expand",
|
|
678
679
|
"external_link",
|
|
679
680
|
"eye_closed",
|
|
@@ -11,7 +11,7 @@ from reflex.components.core.breakpoints import Breakpoints
|
|
|
11
11
|
from reflex.event import EventType, PointerEventInfo
|
|
12
12
|
from reflex.vars.base import Var
|
|
13
13
|
|
|
14
|
-
LUCIDE_LIBRARY = "lucide-react@0.
|
|
14
|
+
LUCIDE_LIBRARY = "lucide-react@0.544.0"
|
|
15
15
|
|
|
16
16
|
class LucideIconComponent(Component):
|
|
17
17
|
@classmethod
|
|
@@ -739,6 +739,7 @@ LUCIDE_ICON_LIST = [
|
|
|
739
739
|
"eraser",
|
|
740
740
|
"ethernet_port",
|
|
741
741
|
"euro",
|
|
742
|
+
"ev_charger",
|
|
742
743
|
"expand",
|
|
743
744
|
"external_link",
|
|
744
745
|
"eye_closed",
|
reflex/constants/installer.py
CHANGED
|
@@ -14,7 +14,7 @@ class Bun(SimpleNamespace):
|
|
|
14
14
|
"""Bun constants."""
|
|
15
15
|
|
|
16
16
|
# The Bun version.
|
|
17
|
-
VERSION = "1.2.
|
|
17
|
+
VERSION = "1.2.22"
|
|
18
18
|
|
|
19
19
|
# Min Bun Version
|
|
20
20
|
MIN_VERSION = "1.2.17"
|
|
@@ -75,7 +75,7 @@ fetch-retries=0
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
def _determine_react_router_version() -> str:
|
|
78
|
-
default_version = "7.
|
|
78
|
+
default_version = "7.9.1"
|
|
79
79
|
if (version := os.getenv("REACT_ROUTER_VERSION")) and version != default_version:
|
|
80
80
|
from reflex.utils import console
|
|
81
81
|
|
|
@@ -143,11 +143,11 @@ class PackageJson(SimpleNamespace):
|
|
|
143
143
|
"postcss-import": "16.1.1",
|
|
144
144
|
"@react-router/dev": _react_router_version,
|
|
145
145
|
"@react-router/fs-routes": _react_router_version,
|
|
146
|
-
"vite": "npm:rolldown-vite@7.1.
|
|
146
|
+
"vite": "npm:rolldown-vite@7.1.9",
|
|
147
147
|
}
|
|
148
148
|
OVERRIDES = {
|
|
149
149
|
# This should always match the `react` version in DEPENDENCIES for recharts compatibility.
|
|
150
150
|
"react-is": _react_version,
|
|
151
151
|
"cookie": "1.0.2",
|
|
152
|
-
"vite": "npm:rolldown-vite@7.1.
|
|
152
|
+
"vite": "npm:rolldown-vite@7.1.9",
|
|
153
153
|
}
|
reflex/event.py
CHANGED
|
@@ -1782,7 +1782,7 @@ def call_event_fn(
|
|
|
1782
1782
|
from reflex.event import EventHandler, EventSpec
|
|
1783
1783
|
from reflex.utils.exceptions import EventHandlerValueError
|
|
1784
1784
|
|
|
1785
|
-
parsed_args,
|
|
1785
|
+
parsed_args, _ = parse_args_spec(arg_spec)
|
|
1786
1786
|
|
|
1787
1787
|
parameters = inspect.signature(fn).parameters
|
|
1788
1788
|
|
reflex/istate/manager.py
CHANGED
|
@@ -355,7 +355,7 @@ class StateManagerDisk(StateManager):
|
|
|
355
355
|
token: The token to set the state for.
|
|
356
356
|
state: The state to set.
|
|
357
357
|
"""
|
|
358
|
-
client_token,
|
|
358
|
+
client_token, _ = _split_substate_key(token)
|
|
359
359
|
await self.set_state_for_substate(client_token, state)
|
|
360
360
|
|
|
361
361
|
@override
|
|
@@ -370,7 +370,7 @@ class StateManagerDisk(StateManager):
|
|
|
370
370
|
The state for the token.
|
|
371
371
|
"""
|
|
372
372
|
# Memory state manager ignores the substate suffix and always returns the top-level state.
|
|
373
|
-
client_token,
|
|
373
|
+
client_token, _ = _split_substate_key(token)
|
|
374
374
|
if client_token not in self._states_locks:
|
|
375
375
|
async with self._state_manager_lock:
|
|
376
376
|
if client_token not in self._states_locks:
|
reflex/istate/proxy.py
CHANGED
|
@@ -71,10 +71,15 @@ class StateProxy(wrapt.ObjectProxy):
|
|
|
71
71
|
state_instance: The state instance to proxy.
|
|
72
72
|
parent_state_proxy: The parent state proxy, for linked mutability and context tracking.
|
|
73
73
|
"""
|
|
74
|
+
from reflex.state import _substate_key
|
|
75
|
+
|
|
74
76
|
super().__init__(state_instance)
|
|
75
|
-
# compile is not relevant to backend logic
|
|
76
77
|
self._self_app = prerequisites.get_and_validate_app().app
|
|
77
78
|
self._self_substate_path = tuple(state_instance.get_full_name().split("."))
|
|
79
|
+
self._self_substate_token = _substate_key(
|
|
80
|
+
state_instance.router.session.client_token,
|
|
81
|
+
self._self_substate_path,
|
|
82
|
+
)
|
|
78
83
|
self._self_actx = None
|
|
79
84
|
self._self_mutable = False
|
|
80
85
|
self._self_actx_lock = asyncio.Lock()
|
|
@@ -127,16 +132,9 @@ class StateProxy(wrapt.ObjectProxy):
|
|
|
127
132
|
msg = "The state is already mutable. Do not nest `async with self` blocks."
|
|
128
133
|
raise ImmutableStateError(msg)
|
|
129
134
|
|
|
130
|
-
from reflex.state import _substate_key
|
|
131
|
-
|
|
132
135
|
await self._self_actx_lock.acquire()
|
|
133
136
|
self._self_actx_lock_holder = current_task
|
|
134
|
-
self._self_actx = self._self_app.modify_state(
|
|
135
|
-
token=_substate_key(
|
|
136
|
-
self.__wrapped__.router.session.client_token,
|
|
137
|
-
self._self_substate_path,
|
|
138
|
-
)
|
|
139
|
-
)
|
|
137
|
+
self._self_actx = self._self_app.modify_state(token=self._self_substate_token)
|
|
140
138
|
mutable_state = await self._self_actx.__aenter__()
|
|
141
139
|
super().__setattr__(
|
|
142
140
|
"__wrapped__", mutable_state.get_substate(self._self_substate_path)
|
|
@@ -378,17 +376,6 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
378
376
|
pydantic.BaseModel.__dict__
|
|
379
377
|
)
|
|
380
378
|
|
|
381
|
-
# These types will be wrapped in MutableProxy
|
|
382
|
-
__mutable_types__ = (
|
|
383
|
-
list,
|
|
384
|
-
dict,
|
|
385
|
-
set,
|
|
386
|
-
Base,
|
|
387
|
-
DeclarativeBase,
|
|
388
|
-
BaseModelV2,
|
|
389
|
-
BaseModelV1,
|
|
390
|
-
)
|
|
391
|
-
|
|
392
379
|
# Dynamically generated classes for tracking dataclass mutations.
|
|
393
380
|
__dataclass_proxies__: dict[type, type] = {}
|
|
394
381
|
|
|
@@ -469,20 +456,6 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
469
456
|
return wrapped(*args, **(kwargs or {}))
|
|
470
457
|
return None
|
|
471
458
|
|
|
472
|
-
@classmethod
|
|
473
|
-
def _is_mutable_type(cls, value: Any) -> bool:
|
|
474
|
-
"""Check if a value is of a mutable type and should be wrapped.
|
|
475
|
-
|
|
476
|
-
Args:
|
|
477
|
-
value: The value to check.
|
|
478
|
-
|
|
479
|
-
Returns:
|
|
480
|
-
Whether the value is of a mutable type.
|
|
481
|
-
"""
|
|
482
|
-
return isinstance(value, cls.__mutable_types__) or (
|
|
483
|
-
dataclasses.is_dataclass(value) and not isinstance(value, Var)
|
|
484
|
-
)
|
|
485
|
-
|
|
486
459
|
@staticmethod
|
|
487
460
|
def _is_called_from_dataclasses_internal() -> bool:
|
|
488
461
|
"""Check if the current function is called from dataclasses helper.
|
|
@@ -514,7 +487,7 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
514
487
|
if self._is_called_from_dataclasses_internal():
|
|
515
488
|
return value
|
|
516
489
|
# Recursively wrap mutable types, but do not re-wrap MutableProxy instances.
|
|
517
|
-
if
|
|
490
|
+
if is_mutable_type(type(value)) and not isinstance(value, MutableProxy):
|
|
518
491
|
base_cls = globals()[self.__base_proxy__]
|
|
519
492
|
return base_cls(
|
|
520
493
|
wrapped=value,
|
|
@@ -575,7 +548,7 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
575
548
|
self._wrap_recursive_decorator,
|
|
576
549
|
)
|
|
577
550
|
|
|
578
|
-
if
|
|
551
|
+
if is_mutable_type(type(value)) and __name not in (
|
|
579
552
|
"__wrapped__",
|
|
580
553
|
"_self_state",
|
|
581
554
|
"__dict__",
|
|
@@ -764,3 +737,30 @@ class ImmutableMutableProxy(MutableProxy):
|
|
|
764
737
|
return super()._mark_dirty(
|
|
765
738
|
wrapped=wrapped, instance=instance, args=args, kwargs=kwargs
|
|
766
739
|
)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
# These types will be wrapped in MutableProxy
|
|
743
|
+
MUTABLE_TYPES = (
|
|
744
|
+
list,
|
|
745
|
+
dict,
|
|
746
|
+
set,
|
|
747
|
+
Base,
|
|
748
|
+
DeclarativeBase,
|
|
749
|
+
BaseModelV2,
|
|
750
|
+
BaseModelV1,
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@functools.lru_cache(maxsize=1024)
|
|
755
|
+
def is_mutable_type(type_: type) -> bool:
|
|
756
|
+
"""Check if a type is mutable and should be wrapped.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
type_: The type to check.
|
|
760
|
+
|
|
761
|
+
Returns:
|
|
762
|
+
Whether the type is mutable and should be wrapped.
|
|
763
|
+
"""
|
|
764
|
+
return issubclass(type_, MUTABLE_TYPES) or (
|
|
765
|
+
dataclasses.is_dataclass(type_) and not issubclass(type_, Var)
|
|
766
|
+
)
|