Compare commits
5 Commits
main
...
lendemor/f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d2b183e3f9 | ||
![]() |
2f215a350e | ||
![]() |
2825500e26 | ||
![]() |
41d8cfae57 | ||
![]() |
b11328160a |
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "reflex"
|
||||
version = "0.7.2dev1"
|
||||
version = "0.7.1dev1"
|
||||
description = "Web apps in pure Python."
|
||||
license = "Apache-2.0"
|
||||
authors = [
|
||||
|
@ -15,13 +15,7 @@
|
||||
"devDependencies": {
|
||||
{% for package, version in dev_dependencies.items() %}
|
||||
"{{ package }}": "{{ version }}"{% if not loop.last %},{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
},
|
||||
"overrides": {
|
||||
{% for package, version in overrides.items() %}
|
||||
"{{ package }}": "{{ version }}"{% if not loop.last %},{% endif %}
|
||||
|
||||
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
@ -78,9 +78,9 @@ export function UploadFilesProvider({ children }) {
|
||||
return newFilesById
|
||||
})
|
||||
return (
|
||||
<UploadFilesContext value={[filesById, setFilesById]}>
|
||||
<UploadFilesContext.Provider value={[filesById, setFilesById]}>
|
||||
{children}
|
||||
</UploadFilesContext>
|
||||
</UploadFilesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@ -92,9 +92,9 @@ export function EventLoopProvider({ children }) {
|
||||
clientStorage,
|
||||
)
|
||||
return (
|
||||
<EventLoopContext value={[addEvents, connectErrors]}>
|
||||
<EventLoopContext.Provider value={[addEvents, connectErrors]}>
|
||||
{children}
|
||||
</EventLoopContext>
|
||||
</EventLoopContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@ -112,13 +112,13 @@ export function StateProvider({ children }) {
|
||||
|
||||
return (
|
||||
{% for state_name in initial_state %}
|
||||
<StateContexts.{{state_name|var_name}} value={ {{state_name|var_name}} }>
|
||||
<StateContexts.{{state_name|var_name}}.Provider value={ {{state_name|var_name}} }>
|
||||
{% endfor %}
|
||||
<DispatchContext value={dispatchers}>
|
||||
<DispatchContext.Provider value={dispatchers}>
|
||||
{children}
|
||||
</DispatchContext>
|
||||
</DispatchContext.Provider>
|
||||
{% for state_name in initial_state|reverse %}
|
||||
</StateContexts.{{state_name|var_name}}>
|
||||
</StateContexts.{{state_name|var_name}}.Provider>
|
||||
{% endfor %}
|
||||
)
|
||||
}
|
||||
|
@ -36,17 +36,17 @@ export default function RadixThemesColorModeProvider({ children }) {
|
||||
const allowedModes = ["light", "dark", "system"];
|
||||
if (!allowedModes.includes(mode)) {
|
||||
console.error(
|
||||
`Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`,
|
||||
`Invalid color mode "${mode}". Defaulting to "${defaultColorMode}".`
|
||||
);
|
||||
mode = defaultColorMode;
|
||||
}
|
||||
setTheme(mode);
|
||||
};
|
||||
return (
|
||||
<ColorModeContext
|
||||
<ColorModeContext.Provider
|
||||
value={{ rawColorMode, resolvedColorMode, toggleColorMode, setColorMode }}
|
||||
>
|
||||
{children}
|
||||
</ColorModeContext>
|
||||
</ColorModeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@ -227,8 +227,8 @@ export const applyEvent = async (event, socket) => {
|
||||
a.href = eval?.(
|
||||
event.payload.url.replace(
|
||||
"getBackendURL(env.UPLOAD)",
|
||||
`"${getBackendURL(env.UPLOAD)}"`,
|
||||
),
|
||||
`"${getBackendURL(env.UPLOAD)}"`
|
||||
)
|
||||
);
|
||||
}
|
||||
a.download = event.payload.filename;
|
||||
@ -341,7 +341,7 @@ export const applyRestEvent = async (event, socket) => {
|
||||
event.payload.files,
|
||||
event.payload.upload_id,
|
||||
event.payload.on_upload_progress,
|
||||
socket,
|
||||
socket
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@ -408,7 +408,7 @@ export const connect = async (
|
||||
dispatch,
|
||||
transports,
|
||||
setConnectErrors,
|
||||
client_storage = {},
|
||||
client_storage = {}
|
||||
) => {
|
||||
// Get backend URL object from the endpoint.
|
||||
const endpoint = getBackendURL(EVENTURL);
|
||||
@ -419,6 +419,7 @@ export const connect = async (
|
||||
transports: transports,
|
||||
protocols: [reflexEnvironment.version],
|
||||
autoUnref: false,
|
||||
query: { token: getToken() },
|
||||
});
|
||||
// Ensure undefined fields in events are sent as null instead of removed
|
||||
socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v);
|
||||
@ -479,6 +480,10 @@ export const connect = async (
|
||||
event_processing = false;
|
||||
queueEvents([...initialEvents(), event], socket);
|
||||
});
|
||||
socket.current.on("new_token", async (new_token) => {
|
||||
token = new_token;
|
||||
window.sessionStorage.setItem(TOKEN_KEY, new_token);
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", checkVisibility);
|
||||
};
|
||||
@ -499,7 +504,7 @@ export const uploadFiles = async (
|
||||
files,
|
||||
upload_id,
|
||||
on_upload_progress,
|
||||
socket,
|
||||
socket
|
||||
) => {
|
||||
// return if there's no file to upload
|
||||
if (files === undefined || files.length === 0) {
|
||||
@ -604,7 +609,7 @@ export const Event = (
|
||||
name,
|
||||
payload = {},
|
||||
event_actions = {},
|
||||
handler = null,
|
||||
handler = null
|
||||
) => {
|
||||
return { name, payload, handler, event_actions };
|
||||
};
|
||||
@ -631,7 +636,7 @@ export const hydrateClientStorage = (client_storage) => {
|
||||
for (const state_key in client_storage.local_storage) {
|
||||
const options = client_storage.local_storage[state_key];
|
||||
const local_storage_value = localStorage.getItem(
|
||||
options.name || state_key,
|
||||
options.name || state_key
|
||||
);
|
||||
if (local_storage_value !== null) {
|
||||
client_storage_values[state_key] = local_storage_value;
|
||||
@ -642,7 +647,7 @@ export const hydrateClientStorage = (client_storage) => {
|
||||
for (const state_key in client_storage.session_storage) {
|
||||
const session_options = client_storage.session_storage[state_key];
|
||||
const session_storage_value = sessionStorage.getItem(
|
||||
session_options.name || state_key,
|
||||
session_options.name || state_key
|
||||
);
|
||||
if (session_storage_value != null) {
|
||||
client_storage_values[state_key] = session_storage_value;
|
||||
@ -667,7 +672,7 @@ export const hydrateClientStorage = (client_storage) => {
|
||||
const applyClientStorageDelta = (client_storage, delta) => {
|
||||
// find the main state and check for is_hydrated
|
||||
const unqualified_states = Object.keys(delta).filter(
|
||||
(key) => key.split(".").length === 1,
|
||||
(key) => key.split(".").length === 1
|
||||
);
|
||||
if (unqualified_states.length === 1) {
|
||||
const main_state = delta[unqualified_states[0]];
|
||||
@ -701,7 +706,7 @@ const applyClientStorageDelta = (client_storage, delta) => {
|
||||
const session_options = client_storage.session_storage[state_key];
|
||||
sessionStorage.setItem(
|
||||
session_options.name || state_key,
|
||||
delta[substate][key],
|
||||
delta[substate][key]
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -721,7 +726,7 @@ const applyClientStorageDelta = (client_storage, delta) => {
|
||||
export const useEventLoop = (
|
||||
dispatch,
|
||||
initial_events = () => [],
|
||||
client_storage = {},
|
||||
client_storage = {}
|
||||
) => {
|
||||
const socket = useRef(null);
|
||||
const router = useRouter();
|
||||
@ -735,7 +740,7 @@ export const useEventLoop = (
|
||||
|
||||
event_actions = events.reduce(
|
||||
(acc, e) => ({ ...acc, ...e.event_actions }),
|
||||
event_actions ?? {},
|
||||
event_actions ?? {}
|
||||
);
|
||||
|
||||
const _e = args.filter((o) => o?.preventDefault !== undefined)[0];
|
||||
@ -763,7 +768,7 @@ export const useEventLoop = (
|
||||
debounce(
|
||||
combined_name,
|
||||
() => queueEvents(events, socket),
|
||||
event_actions.debounce,
|
||||
event_actions.debounce
|
||||
);
|
||||
} else {
|
||||
queueEvents(events, socket);
|
||||
@ -782,7 +787,7 @@ export const useEventLoop = (
|
||||
query,
|
||||
asPath,
|
||||
}))(router),
|
||||
})),
|
||||
}))
|
||||
);
|
||||
sentHydrate.current = true;
|
||||
}
|
||||
@ -828,7 +833,7 @@ export const useEventLoop = (
|
||||
dispatch,
|
||||
["websocket"],
|
||||
setConnectErrors,
|
||||
client_storage,
|
||||
client_storage
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -876,7 +881,7 @@ export const useEventLoop = (
|
||||
vars[storage_to_state_map[e.key]] = e.newValue;
|
||||
const event = Event(
|
||||
`${state_name}.reflex___state____update_vars_internal_state.update_vars_internal`,
|
||||
{ vars: vars },
|
||||
{ vars: vars }
|
||||
);
|
||||
addEvents([event], e);
|
||||
}
|
||||
@ -969,7 +974,7 @@ export const getRefValues = (refs) => {
|
||||
return refs.map((ref) =>
|
||||
ref.current
|
||||
? ref.current.value || ref.current.getAttribute("aria-valuenow")
|
||||
: null,
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -13,6 +13,8 @@ import io
|
||||
import json
|
||||
import sys
|
||||
import traceback
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from timeit import default_timer as timer
|
||||
@ -75,7 +77,6 @@ from reflex.components.core.client_side_routing import (
|
||||
from reflex.components.core.sticky import sticky
|
||||
from reflex.components.core.upload import Upload, get_upload_dir
|
||||
from reflex.components.radix import themes
|
||||
from reflex.components.sonner.toast import toast
|
||||
from reflex.config import ExecutorType, environment, get_config
|
||||
from reflex.event import (
|
||||
_EVENT_FIELDS,
|
||||
@ -85,6 +86,7 @@ from reflex.event import (
|
||||
EventType,
|
||||
IndividualEventType,
|
||||
get_hydrate_event,
|
||||
window_alert,
|
||||
)
|
||||
from reflex.model import Model, get_db_status
|
||||
from reflex.page import DECORATED_PAGES
|
||||
@ -144,7 +146,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
|
||||
EventSpec: The window alert event.
|
||||
|
||||
"""
|
||||
from reflex.components.sonner.toast import toast
|
||||
from reflex.components.sonner.toast import Toaster, toast
|
||||
|
||||
error = traceback.format_exc()
|
||||
|
||||
@ -155,16 +157,18 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
|
||||
if is_prod_mode()
|
||||
else [f"{type(exception).__name__}: {exception}.", "See logs for details."]
|
||||
)
|
||||
|
||||
return toast(
|
||||
"An error occurred.",
|
||||
level="error",
|
||||
fallback_to_alert=True,
|
||||
description="<br/>".join(error_message),
|
||||
position="top-center",
|
||||
id="backend_error",
|
||||
style={"width": "500px"},
|
||||
)
|
||||
if Toaster.is_used:
|
||||
return toast(
|
||||
"An error occurred.",
|
||||
level="error",
|
||||
description="<br/>".join(error_message),
|
||||
position="top-center",
|
||||
id="backend_error",
|
||||
style={"width": "500px"},
|
||||
)
|
||||
else:
|
||||
error_message.insert(0, "An error occurred.")
|
||||
return window_alert("\n".join(error_message))
|
||||
|
||||
|
||||
def extra_overlay_function() -> Optional[Component]:
|
||||
@ -412,7 +416,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
||||
] = default_backend_exception_handler
|
||||
|
||||
# Put the toast provider in the app wrap.
|
||||
toaster: Component | None = dataclasses.field(default_factory=toast.provider)
|
||||
bundle_toaster: bool = True
|
||||
|
||||
@property
|
||||
def api(self) -> FastAPI | None:
|
||||
@ -1098,6 +1102,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
||||
should_compile = self._should_compile()
|
||||
|
||||
if not should_compile:
|
||||
if self.bundle_toaster:
|
||||
from reflex.components.sonner.toast import Toaster
|
||||
|
||||
Toaster.is_used = True
|
||||
with console.timing("Evaluate Pages (Backend)"):
|
||||
for route in self._unevaluated_pages:
|
||||
console.debug(f"Evaluating page: {route}")
|
||||
@ -1127,6 +1135,20 @@ class App(MiddlewareMixin, LifespanMixin):
|
||||
+ adhoc_steps_without_executor,
|
||||
)
|
||||
|
||||
if self.bundle_toaster:
|
||||
from reflex.components.component import memo
|
||||
from reflex.components.sonner.toast import toast
|
||||
|
||||
internal_toast_provider = toast.provider()
|
||||
|
||||
@memo
|
||||
def memoized_toast_provider():
|
||||
return internal_toast_provider
|
||||
|
||||
toast_provider = Fragment.create(memoized_toast_provider())
|
||||
|
||||
app_wrappers[(1, "ToasterProvider")] = toast_provider
|
||||
|
||||
with console.timing("Evaluate Pages (Frontend)"):
|
||||
performance_metrics: list[tuple[str, float]] = []
|
||||
for route in self._unevaluated_pages:
|
||||
@ -1187,17 +1209,6 @@ class App(MiddlewareMixin, LifespanMixin):
|
||||
# Add the custom components from the page to the set.
|
||||
custom_components |= component._get_all_custom_components()
|
||||
|
||||
if (toaster := self.toaster) is not None:
|
||||
from reflex.components.component import memo
|
||||
|
||||
@memo
|
||||
def memoized_toast_provider():
|
||||
return toaster
|
||||
|
||||
toast_provider = Fragment.create(memoized_toast_provider())
|
||||
|
||||
app_wrappers[(1, "ToasterProvider")] = toast_provider
|
||||
|
||||
# Add the app wraps to the app.
|
||||
for key, app_wrap in self.app_wraps.items():
|
||||
component = app_wrap(self._state is not None)
|
||||
@ -1825,13 +1836,16 @@ class EventNamespace(AsyncNamespace):
|
||||
self.sid_to_token = {}
|
||||
self.app = app
|
||||
|
||||
def on_connect(self, sid: str, environ: dict):
|
||||
async def on_connect(self, sid: str, environ: dict):
|
||||
"""Event for when the websocket is connected.
|
||||
|
||||
Args:
|
||||
sid: The Socket.IO session id.
|
||||
environ: The request information, including HTTP headers.
|
||||
"""
|
||||
query_params = urllib.parse.parse_qs(environ.get("QUERY_STRING"))
|
||||
await self.link_token_to_sid(sid, query_params.get("token", [])[0])
|
||||
|
||||
subprotocol = environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL")
|
||||
if subprotocol and subprotocol != constants.Reflex.VERSION:
|
||||
console.warn(
|
||||
@ -1900,9 +1914,6 @@ class EventNamespace(AsyncNamespace):
|
||||
f"Failed to deserialize event data: {fields}."
|
||||
) from ex
|
||||
|
||||
self.token_to_sid[event.token] = sid
|
||||
self.sid_to_token[sid] = event.token
|
||||
|
||||
# Get the event environment.
|
||||
if self.app.sio is None:
|
||||
raise RuntimeError("Socket.IO is not initialized.")
|
||||
@ -1935,3 +1946,17 @@ class EventNamespace(AsyncNamespace):
|
||||
"""
|
||||
# Emit the test event.
|
||||
await self.emit(str(constants.SocketEvent.PING), "pong", to=sid)
|
||||
|
||||
async def link_token_to_sid(self, sid: str, token: str):
|
||||
"""Link a token to a session id.
|
||||
|
||||
Args:
|
||||
sid: The Socket.IO session id.
|
||||
token: The client token.
|
||||
"""
|
||||
if token in self.sid_to_token.values() and sid != self.token_to_sid.get(token):
|
||||
token = str(uuid.uuid4())
|
||||
await self.emit("new_token", token, to=sid)
|
||||
|
||||
self.token_to_sid[token] = sid
|
||||
self.sid_to_token[sid] = token
|
||||
|
@ -11,9 +11,7 @@ from reflex.vars.base import Var, get_unique_variable_name
|
||||
class AutoScroll(Div):
|
||||
"""A div that automatically scrolls to the bottom when new content is added."""
|
||||
|
||||
_memoization_mode = MemoizationMode(
|
||||
disposition=MemoizationDisposition.ALWAYS, recursive=False
|
||||
)
|
||||
_memoization_mode = MemoizationMode(disposition=MemoizationDisposition.ALWAYS)
|
||||
|
||||
@classmethod
|
||||
def create(cls, *children, **props):
|
||||
@ -46,6 +44,7 @@ class AutoScroll(Div):
|
||||
"""
|
||||
ref_name = self.get_ref()
|
||||
return [
|
||||
"const containerRef = useRef(null);",
|
||||
"const wasNearBottom = useRef(false);",
|
||||
"const hadScrollbar = useRef(false);",
|
||||
f"""
|
||||
@ -86,8 +85,6 @@ useEffect(() => {{
|
||||
const container = {ref_name}.current;
|
||||
if (!container) return;
|
||||
|
||||
scrollToBottomIfNeeded();
|
||||
|
||||
// Create ResizeObserver to detect height changes
|
||||
const resizeObserver = new ResizeObserver(() => {{
|
||||
scrollToBottomIfNeeded();
|
||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
from reflex import constants
|
||||
from reflex.components.base.fragment import Fragment
|
||||
from reflex.components.component import Component
|
||||
from reflex.components.core.cond import cond
|
||||
from reflex.components.el.elements.typography import Div
|
||||
@ -17,7 +16,7 @@ from reflex.components.radix.themes.components.dialog import (
|
||||
)
|
||||
from reflex.components.radix.themes.layout.flex import Flex
|
||||
from reflex.components.radix.themes.typography.text import Text
|
||||
from reflex.components.sonner.toast import ToastProps, toast_ref
|
||||
from reflex.components.sonner.toast import Toaster, ToastProps
|
||||
from reflex.config import environment
|
||||
from reflex.constants import Dirs, Hooks, Imports
|
||||
from reflex.constants.compiler import CompileVars
|
||||
@ -91,7 +90,7 @@ def default_connection_error() -> list[str | Var | Component]:
|
||||
]
|
||||
|
||||
|
||||
class ConnectionToaster(Fragment):
|
||||
class ConnectionToaster(Toaster):
|
||||
"""A connection toaster component."""
|
||||
|
||||
def add_hooks(self) -> list[str | Var]:
|
||||
@ -114,11 +113,11 @@ class ConnectionToaster(Fragment):
|
||||
if environment.REFLEX_DOES_BACKEND_COLD_START.get():
|
||||
loading_message = Var.create("Backend is starting.")
|
||||
backend_is_loading_toast_var = Var(
|
||||
f"toast?.loading({loading_message!s}, {{...toast_props, description: '', closeButton: false, onDismiss: () => setUserDismissed(true)}},)"
|
||||
f"toast.loading({loading_message!s}, {{...toast_props, description: '', closeButton: false, onDismiss: () => setUserDismissed(true)}},)"
|
||||
)
|
||||
backend_is_not_responding = Var.create("Backend is not responding.")
|
||||
backend_is_down_toast_var = Var(
|
||||
f"toast?.error({backend_is_not_responding!s}, {{...toast_props, description: '', onDismiss: () => setUserDismissed(true)}},)"
|
||||
f"toast.error({backend_is_not_responding!s}, {{...toast_props, description: '', onDismiss: () => setUserDismissed(true)}},)"
|
||||
)
|
||||
toast_var = Var(
|
||||
f"""
|
||||
@ -139,11 +138,10 @@ setTimeout(() => {{
|
||||
f"Cannot connect to server: {connection_error}."
|
||||
)
|
||||
toast_var = Var(
|
||||
f"toast?.error({loading_message!s}, {{...toast_props, onDismiss: () => setUserDismissed(true)}},)"
|
||||
f"toast.error({loading_message!s}, {{...toast_props, onDismiss: () => setUserDismissed(true)}},)"
|
||||
)
|
||||
|
||||
individual_hooks = [
|
||||
Var(f"const toast = {toast_ref};"),
|
||||
f"const toast_props = {LiteralVar.create(props)!s};",
|
||||
"const [userDismissed, setUserDismissed] = useState(false);",
|
||||
"const [waitedForBackend, setWaitedForBackend] = useState(false);",
|
||||
@ -165,7 +163,7 @@ setTimeout(() => {{
|
||||
{toast_var!s}
|
||||
}}
|
||||
}} else {{
|
||||
toast?.dismiss("{toast_id}");
|
||||
toast.dismiss("{toast_id}");
|
||||
setUserDismissed(false); // after reconnection reset dismissed state
|
||||
}}
|
||||
}}
|
||||
@ -191,6 +189,7 @@ setTimeout(() => {{
|
||||
Returns:
|
||||
The connection toaster component.
|
||||
"""
|
||||
Toaster.is_used = True
|
||||
return super().create(*children, **props)
|
||||
|
||||
|
||||
|
@ -5,10 +5,10 @@
|
||||
# ------------------------------------------------------
|
||||
from typing import Any, Dict, Literal, Optional, Union, overload
|
||||
|
||||
from reflex.components.base.fragment import Fragment
|
||||
from reflex.components.component import Component
|
||||
from reflex.components.el.elements.typography import Div
|
||||
from reflex.components.lucide.icon import Icon
|
||||
from reflex.components.sonner.toast import Toaster, ToastProps
|
||||
from reflex.constants.compiler import CompileVars
|
||||
from reflex.event import EventType
|
||||
from reflex.style import Style
|
||||
@ -41,13 +41,48 @@ class WebsocketTargetURL(Var):
|
||||
|
||||
def default_connection_error() -> list[str | Var | Component]: ...
|
||||
|
||||
class ConnectionToaster(Fragment):
|
||||
class ConnectionToaster(Toaster):
|
||||
def add_hooks(self) -> list[str | Var]: ...
|
||||
@overload
|
||||
@classmethod
|
||||
def create( # type: ignore
|
||||
cls,
|
||||
*children,
|
||||
theme: Optional[Union[Var[str], str]] = None,
|
||||
rich_colors: Optional[Union[Var[bool], bool]] = None,
|
||||
expand: Optional[Union[Var[bool], bool]] = None,
|
||||
visible_toasts: Optional[Union[Var[int], int]] = None,
|
||||
position: Optional[
|
||||
Union[
|
||||
Literal[
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"bottom-right",
|
||||
"top-center",
|
||||
"top-left",
|
||||
"top-right",
|
||||
],
|
||||
Var[
|
||||
Literal[
|
||||
"bottom-center",
|
||||
"bottom-left",
|
||||
"bottom-right",
|
||||
"top-center",
|
||||
"top-left",
|
||||
"top-right",
|
||||
]
|
||||
],
|
||||
]
|
||||
] = None,
|
||||
close_button: Optional[Union[Var[bool], bool]] = None,
|
||||
offset: Optional[Union[Var[str], str]] = None,
|
||||
dir: Optional[Union[Var[str], str]] = None,
|
||||
hotkey: Optional[Union[Var[str], str]] = None,
|
||||
invert: Optional[Union[Var[bool], bool]] = None,
|
||||
toast_options: Optional[Union[ToastProps, Var[ToastProps]]] = None,
|
||||
gap: Optional[Union[Var[int], int]] = None,
|
||||
loading_icon: Optional[Union[Icon, Var[Icon]]] = None,
|
||||
pause_when_page_is_hidden: Optional[Union[Var[bool], bool]] = None,
|
||||
style: Optional[Style] = None,
|
||||
key: Optional[Any] = None,
|
||||
id: Optional[Any] = None,
|
||||
@ -75,6 +110,20 @@ class ConnectionToaster(Fragment):
|
||||
|
||||
Args:
|
||||
*children: The children of the component.
|
||||
theme: the theme of the toast
|
||||
rich_colors: whether to show rich colors
|
||||
expand: whether to expand the toast
|
||||
visible_toasts: the number of toasts that are currently visible
|
||||
position: the position of the toast
|
||||
close_button: whether to show the close button
|
||||
offset: offset of the toast
|
||||
dir: directionality of the toast (default: ltr)
|
||||
hotkey: Keyboard shortcut that will move focus to the toaster area.
|
||||
invert: Dark toasts in light mode and vice versa.
|
||||
toast_options: These will act as default options for all toasts. See toast() for all available options.
|
||||
gap: Gap between toasts when expanded
|
||||
loading_icon: Changes the default loading icon
|
||||
pause_when_page_is_hidden: Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
|
||||
style: The style of the component.
|
||||
key: A unique key for the component.
|
||||
id: The id for the component.
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal, Optional, Union
|
||||
from typing import Any, ClassVar, Literal, Optional, Union
|
||||
|
||||
from reflex.base import Base
|
||||
from reflex.components.component import Component, ComponentNamespace
|
||||
@ -17,7 +17,6 @@ from reflex.utils.serializers import serializer
|
||||
from reflex.vars import VarData
|
||||
from reflex.vars.base import LiteralVar, Var
|
||||
from reflex.vars.function import FunctionVar
|
||||
from reflex.vars.number import ternary_operation
|
||||
from reflex.vars.object import ObjectVar
|
||||
|
||||
LiteralPosition = Literal[
|
||||
@ -218,6 +217,9 @@ class Toaster(Component):
|
||||
# Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
|
||||
pause_when_page_is_hidden: Var[bool]
|
||||
|
||||
# Marked True when any Toast component is created.
|
||||
is_used: ClassVar[bool] = False
|
||||
|
||||
def add_hooks(self) -> list[Var | str]:
|
||||
"""Add hooks for the toaster component.
|
||||
|
||||
@ -239,17 +241,13 @@ class Toaster(Component):
|
||||
|
||||
@staticmethod
|
||||
def send_toast(
|
||||
message: str | Var = "",
|
||||
level: str | None = None,
|
||||
fallback_to_alert: bool = False,
|
||||
**props,
|
||||
message: str | Var = "", level: str | None = None, **props
|
||||
) -> EventSpec:
|
||||
"""Send a toast message.
|
||||
|
||||
Args:
|
||||
message: The message to display.
|
||||
level: The level of the toast.
|
||||
fallback_to_alert: Whether to fallback to an alert if the toaster is not created.
|
||||
**props: The options for the toast.
|
||||
|
||||
Raises:
|
||||
@ -258,6 +256,11 @@ class Toaster(Component):
|
||||
Returns:
|
||||
The toast event.
|
||||
"""
|
||||
if not Toaster.is_used:
|
||||
raise ValueError(
|
||||
"Toaster component must be created before sending a toast. (use `rx.toast.provider()`)"
|
||||
)
|
||||
|
||||
toast_command = (
|
||||
ObjectVar.__getattr__(toast_ref.to(dict), level) if level else toast_ref
|
||||
).to(FunctionVar)
|
||||
@ -274,21 +277,6 @@ class Toaster(Component):
|
||||
else:
|
||||
toast = toast_command.call(message)
|
||||
|
||||
if fallback_to_alert:
|
||||
toast = ternary_operation(
|
||||
toast_ref.bool(),
|
||||
toast,
|
||||
FunctionVar("window.alert").call(
|
||||
Var.create(
|
||||
message
|
||||
if isinstance(message, str) and message
|
||||
else props.get("title", props.get("description", ""))
|
||||
)
|
||||
.to(str)
|
||||
.replace("<br/>", "\n")
|
||||
),
|
||||
)
|
||||
|
||||
return run_script(toast)
|
||||
|
||||
@staticmethod
|
||||
@ -391,6 +379,7 @@ class Toaster(Component):
|
||||
Returns:
|
||||
The toaster component.
|
||||
"""
|
||||
cls.is_used = True
|
||||
return super().create(*children, **props)
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
# ------------------- DO NOT EDIT ----------------------
|
||||
# This file was generated by `reflex/utils/pyi_generator.py`!
|
||||
# ------------------------------------------------------
|
||||
from typing import Any, Dict, Literal, Optional, Union, overload
|
||||
from typing import Any, ClassVar, Dict, Literal, Optional, Union, overload
|
||||
|
||||
from reflex.base import Base
|
||||
from reflex.components.component import Component, ComponentNamespace
|
||||
@ -60,13 +60,12 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
|
||||
def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
|
||||
|
||||
class Toaster(Component):
|
||||
is_used: ClassVar[bool] = False
|
||||
|
||||
def add_hooks(self) -> list[Var | str]: ...
|
||||
@staticmethod
|
||||
def send_toast(
|
||||
message: str | Var = "",
|
||||
level: str | None = None,
|
||||
fallback_to_alert: bool = False,
|
||||
**props,
|
||||
message: str | Var = "", level: str | None = None, **props
|
||||
) -> EventSpec: ...
|
||||
@staticmethod
|
||||
def toast_info(message: str | Var = "", **kwargs: Any): ...
|
||||
@ -186,17 +185,13 @@ class ToastNamespace(ComponentNamespace):
|
||||
|
||||
@staticmethod
|
||||
def __call__(
|
||||
message: Union[str, Var] = "",
|
||||
level: Optional[str] = None,
|
||||
fallback_to_alert: bool = False,
|
||||
**props,
|
||||
message: Union[str, Var] = "", level: Optional[str] = None, **props
|
||||
) -> "EventSpec":
|
||||
"""Send a toast message.
|
||||
|
||||
Args:
|
||||
message: The message to display.
|
||||
level: The level of the toast.
|
||||
fallback_to_alert: Whether to fallback to an alert if the toaster is not created.
|
||||
**props: The options for the toast.
|
||||
|
||||
Raises:
|
||||
|
@ -195,7 +195,3 @@ class PackageJson(SimpleNamespace):
|
||||
"postcss": "8.5.1",
|
||||
"postcss-import": "16.1.0",
|
||||
}
|
||||
OVERRIDES = {
|
||||
# This should always match the `react` version in DEPENDENCIES for recharts compatibility.
|
||||
"react-is": "19.0.0"
|
||||
}
|
||||
|
@ -846,7 +846,6 @@ def _compile_package_json():
|
||||
},
|
||||
dependencies=constants.PackageJson.DEPENDENCIES,
|
||||
dev_dependencies=constants.PackageJson.DEV_DEPENDENCIES,
|
||||
overrides=constants.PackageJson.OVERRIDES,
|
||||
)
|
||||
|
||||
|
||||
|
@ -35,6 +35,7 @@ import reflex.config
|
||||
from reflex import constants
|
||||
from reflex.app import App
|
||||
from reflex.base import Base
|
||||
from reflex.components.sonner.toast import Toaster
|
||||
from reflex.constants import CompileVars, RouteVar, SocketEvent
|
||||
from reflex.event import Event, EventHandler
|
||||
from reflex.state import (
|
||||
@ -1612,20 +1613,29 @@ async def test_state_with_invalid_yield(capsys, mock_app):
|
||||
rx.event.Event(token="fake_token", name="invalid_handler")
|
||||
):
|
||||
assert not update.delta
|
||||
assert update.events == rx.event.fix_events(
|
||||
[
|
||||
rx.toast(
|
||||
"An error occurred.",
|
||||
level="error",
|
||||
fallback_to_alert=True,
|
||||
description="TypeError: Your handler test_state_with_invalid_yield.<locals>.StateWithInvalidYield.invalid_handler must only return/yield: None, Events or other EventHandlers referenced by their class (not using `self`).<br/>See logs for details.",
|
||||
id="backend_error",
|
||||
position="top-center",
|
||||
style={"width": "500px"},
|
||||
)
|
||||
],
|
||||
token="",
|
||||
)
|
||||
if Toaster.is_used:
|
||||
assert update.events == rx.event.fix_events(
|
||||
[
|
||||
rx.toast(
|
||||
"An error occurred.",
|
||||
description="TypeError: Your handler test_state_with_invalid_yield.<locals>.StateWithInvalidYield.invalid_handler must only return/yield: None, Events or other EventHandlers referenced by their class (not using `self`).<br/>See logs for details.",
|
||||
level="error",
|
||||
id="backend_error",
|
||||
position="top-center",
|
||||
style={"width": "500px"},
|
||||
)
|
||||
],
|
||||
token="",
|
||||
)
|
||||
else:
|
||||
assert update.events == rx.event.fix_events(
|
||||
[
|
||||
rx.window_alert(
|
||||
"An error occurred.\nContact the website administrator."
|
||||
)
|
||||
],
|
||||
token="",
|
||||
)
|
||||
captured = capsys.readouterr()
|
||||
assert "must only return/yield: None, Events or other EventHandlers" in captured.out
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user