diff --git a/reflex/app.py b/reflex/app.py index 658ba1a1f..7e40a95bf 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -112,11 +112,29 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec: EventSpec: The window alert event. """ + from reflex.components.sonner.toast import Toaster, toast + error = traceback.format_exc() console.error(f"[Reflex Backend Exception]\n {error}\n") - return window_alert("An error occurred. See logs for details.") + error_message = ( + ["Contact the website administrator."] + if is_prod_mode() + else [f"{type(exception).__name__}: {exception}.", "See logs for details."] + ) + if Toaster.is_used: + return toast( + level="error", + title="An error occurred.", + description="
".join(error_message), + position="top-center", + id="backend_error", + style={"width": "500px"}, + ) # type: ignore + else: + error_message.insert(0, "An error occurred.") + return window_alert("\n".join(error_message)) def default_overlay_component() -> Component: @@ -183,7 +201,7 @@ class App(MiddlewareMixin, LifespanMixin, Base): # A component that is present on every page (defaults to the Connection Error banner). overlay_component: Optional[Union[Component, ComponentCallable]] = ( - default_overlay_component + default_overlay_component() ) # Error boundary component to wrap the app with. diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py index b634ab75a..c6b46696c 100644 --- a/reflex/components/core/banner.py +++ b/reflex/components/core/banner.py @@ -153,6 +153,20 @@ useEffect(() => {{ hook, ] + @classmethod + def create(cls, *children, **props) -> Component: + """Create a connection toaster component. + + Args: + *children: The children of the component. + **props: The properties of the component. + + Returns: + The connection toaster component. + """ + Toaster.is_used = True + return super().create(*children, **props) + class ConnectionBanner(Component): """A connection banner component.""" diff --git a/reflex/components/core/banner.pyi b/reflex/components/core/banner.pyi index ddaafb153..b9b6d506f 100644 --- a/reflex/components/core/banner.pyi +++ b/reflex/components/core/banner.pyi @@ -187,7 +187,7 @@ class ConnectionToaster(Toaster): ] = None, **props, ) -> "ConnectionToaster": - """Create the component. + """Create a connection toaster component. Args: *children: The children of the component. @@ -211,10 +211,10 @@ class ConnectionToaster(Toaster): class_name: The class name for the component. autofocus: Whether the component should take the focus once the page is loaded custom_attrs: custom attribute - **props: The props of the component. + **props: The properties of the component. Returns: - The component. + The connection toaster component. """ ... diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index f8d1cc340..d4df31e82 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -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 @@ -211,6 +211,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. @@ -231,7 +234,7 @@ class Toaster(Component): return [hook] @staticmethod - def send_toast(message: str, level: str | None = None, **props) -> EventSpec: + def send_toast(message: str = "", level: str | None = None, **props) -> EventSpec: """Send a toast message. Args: @@ -239,10 +242,19 @@ class Toaster(Component): level: The level of the toast. **props: The options for the toast. + Raises: + ValueError: If the Toaster component is not created. + 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 = f"{toast_ref}.{level}" if level is not None else toast_ref + if message == "" and ("title" not in props or "description" not in props): + raise ValueError("Toast message or title or description must be provided.") if props: args = serialize(ToastProps(**props)) # type: ignore toast = f"{toast_command}(`{message}`, {args})" @@ -331,6 +343,20 @@ class Toaster(Component): ) return call_script(dismiss_action) + @classmethod + def create(cls, *children, **props) -> Component: + """Create a toaster component. + + Args: + *children: The children of the toaster. + **props: The properties of the toaster. + + Returns: + The toaster component. + """ + cls.is_used = True + return super().create(*children, **props) + # TODO: figure out why loading toast stay open forever # def toast_loading(message: str, **kwargs): diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi index 82999a06a..7e5758b16 100644 --- a/reflex/components/sonner/toast.pyi +++ b/reflex/components/sonner/toast.pyi @@ -3,7 +3,7 @@ # ------------------- DO NOT EDIT ---------------------- # This file was generated by `reflex/utils/pyi_generator.py`! # ------------------------------------------------------ -from typing import Any, Callable, Dict, Literal, Optional, Union, overload +from typing import Any, Callable, ClassVar, Dict, Literal, Optional, Union, overload from reflex.base import Base from reflex.components.component import Component, ComponentNamespace @@ -52,9 +52,13 @@ class ToastProps(PropsBase): def dict(self, *args, **kwargs) -> dict[str, Any]: ... class Toaster(Component): + is_used: ClassVar[bool] = False + def add_hooks(self) -> list[Var | str]: ... @staticmethod - def send_toast(message: str, level: str | None = None, **props) -> EventSpec: ... + def send_toast( + message: str = "", level: str | None = None, **props + ) -> EventSpec: ... @staticmethod def toast_info(message: str, **kwargs): ... @staticmethod @@ -158,10 +162,10 @@ class Toaster(Component): ] = None, **props, ) -> "Toaster": - """Create the component. + """Create a toaster component. Args: - *children: The children of the component. + *children: The children of the toaster. theme: the theme of the toast rich_colors: whether to show rich colors expand: whether to expand the toast @@ -182,10 +186,10 @@ class Toaster(Component): class_name: The class name for the component. autofocus: Whether the component should take the focus once the page is loaded custom_attrs: custom attribute - **props: The props of the component. + **props: The properties of the toaster. Returns: - The component. + The toaster component. """ ... @@ -200,7 +204,7 @@ class ToastNamespace(ComponentNamespace): @staticmethod def __call__( - message: str, level: Optional[str] = None, **props + message: str = "", level: Optional[str] = None, **props ) -> "Optional[EventSpec]": """Send a toast message. @@ -209,6 +213,9 @@ class ToastNamespace(ComponentNamespace): level: The level of the toast. **props: The options for the toast. + Raises: + ValueError: If the Toaster component is not created. + Returns: The toast event. """ diff --git a/reflex/state.py b/reflex/state.py index 49b5bd4a4..9313939dc 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -31,6 +31,8 @@ from typing import ( import dill from sqlalchemy.orm import DeclarativeBase +from reflex.config import get_config + try: import pydantic.v1 as pydantic except ModuleNotFoundError: @@ -42,7 +44,6 @@ from redis.exceptions import ResponseError from reflex import constants from reflex.base import Base -from reflex.config import get_config from reflex.event import ( BACKGROUND_TASK_MARKER, Event, diff --git a/tests/test_state.py b/tests/test_state.py index 2fc149389..c998944ef 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -19,6 +19,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 ( @@ -1527,7 +1528,6 @@ async def test_state_with_invalid_yield(capsys, mock_app): Args: capsys: Pytest fixture for capture standard streams. mock_app: Mock app fixture. - """ class StateWithInvalidYield(BaseState): @@ -1546,10 +1546,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.window_alert("An error occurred. See logs for details.")], - token="", - ) + if Toaster.is_used: + assert update.events == rx.event.fix_events( + [ + rx.toast( + title="An error occurred.", + description="TypeError: Your handler test_state_with_invalid_yield..StateWithInvalidYield.invalid_handler must only return/yield: None, Events or other EventHandlers referenced by their class (not using `self`).
See logs for details.", + level="error", + id="backend_error", + position="top-center", + style={"width": "500px"}, + ) # type: ignore + ], + 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