Ver código fonte

simplify toast banner logic (#4853)

* simplify toast banner logic

* expose toast

* default back to title and desc, and replace brs with new lines
Khaleel Al-Adhami 2 meses atrás
pai
commit
725e47ce8c

+ 24 - 33
reflex/app.py

@@ -75,6 +75,7 @@ from reflex.components.core.client_side_routing import (
 from reflex.components.core.sticky import sticky
 from reflex.components.core.sticky import sticky
 from reflex.components.core.upload import Upload, get_upload_dir
 from reflex.components.core.upload import Upload, get_upload_dir
 from reflex.components.radix import themes
 from reflex.components.radix import themes
+from reflex.components.sonner.toast import toast
 from reflex.config import ExecutorType, environment, get_config
 from reflex.config import ExecutorType, environment, get_config
 from reflex.event import (
 from reflex.event import (
     _EVENT_FIELDS,
     _EVENT_FIELDS,
@@ -84,7 +85,6 @@ from reflex.event import (
     EventType,
     EventType,
     IndividualEventType,
     IndividualEventType,
     get_hydrate_event,
     get_hydrate_event,
-    window_alert,
 )
 )
 from reflex.model import Model, get_db_status
 from reflex.model import Model, get_db_status
 from reflex.page import DECORATED_PAGES
 from reflex.page import DECORATED_PAGES
@@ -144,7 +144,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
         EventSpec: The window alert event.
         EventSpec: The window alert event.
 
 
     """
     """
-    from reflex.components.sonner.toast import Toaster, toast
+    from reflex.components.sonner.toast import toast
 
 
     error = traceback.format_exc()
     error = traceback.format_exc()
 
 
@@ -155,18 +155,16 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
         if is_prod_mode()
         if is_prod_mode()
         else [f"{type(exception).__name__}: {exception}.", "See logs for details."]
         else [f"{type(exception).__name__}: {exception}.", "See logs for details."]
     )
     )
-    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))
+
+    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"},
+    )
 
 
 
 
 def extra_overlay_function() -> Optional[Component]:
 def extra_overlay_function() -> Optional[Component]:
@@ -414,7 +412,7 @@ class App(MiddlewareMixin, LifespanMixin):
     ] = default_backend_exception_handler
     ] = default_backend_exception_handler
 
 
     # Put the toast provider in the app wrap.
     # Put the toast provider in the app wrap.
-    bundle_toaster: bool = True
+    toaster: Component | None = dataclasses.field(default_factory=toast.provider)
 
 
     @property
     @property
     def api(self) -> FastAPI | None:
     def api(self) -> FastAPI | None:
@@ -1100,10 +1098,6 @@ class App(MiddlewareMixin, LifespanMixin):
         should_compile = self._should_compile()
         should_compile = self._should_compile()
 
 
         if not 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)"):
             with console.timing("Evaluate Pages (Backend)"):
                 for route in self._unevaluated_pages:
                 for route in self._unevaluated_pages:
                     console.debug(f"Evaluating page: {route}")
                     console.debug(f"Evaluating page: {route}")
@@ -1133,20 +1127,6 @@ class App(MiddlewareMixin, LifespanMixin):
             + adhoc_steps_without_executor,
             + 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)"):
         with console.timing("Evaluate Pages (Frontend)"):
             performance_metrics: list[tuple[str, float]] = []
             performance_metrics: list[tuple[str, float]] = []
             for route in self._unevaluated_pages:
             for route in self._unevaluated_pages:
@@ -1207,6 +1187,17 @@ class App(MiddlewareMixin, LifespanMixin):
             # Add the custom components from the page to the set.
             # Add the custom components from the page to the set.
             custom_components |= component._get_all_custom_components()
             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.
         # Add the app wraps to the app.
         for key, app_wrap in self.app_wraps.items():
         for key, app_wrap in self.app_wraps.items():
             component = app_wrap(self._state is not None)
             component = app_wrap(self._state is not None)

+ 8 - 7
reflex/components/core/banner.py

@@ -5,6 +5,7 @@ from __future__ import annotations
 from typing import Optional
 from typing import Optional
 
 
 from reflex import constants
 from reflex import constants
+from reflex.components.base.fragment import Fragment
 from reflex.components.component import Component
 from reflex.components.component import Component
 from reflex.components.core.cond import cond
 from reflex.components.core.cond import cond
 from reflex.components.el.elements.typography import Div
 from reflex.components.el.elements.typography import Div
@@ -16,7 +17,7 @@ from reflex.components.radix.themes.components.dialog import (
 )
 )
 from reflex.components.radix.themes.layout.flex import Flex
 from reflex.components.radix.themes.layout.flex import Flex
 from reflex.components.radix.themes.typography.text import Text
 from reflex.components.radix.themes.typography.text import Text
-from reflex.components.sonner.toast import Toaster, ToastProps
+from reflex.components.sonner.toast import ToastProps, toast_ref
 from reflex.config import environment
 from reflex.config import environment
 from reflex.constants import Dirs, Hooks, Imports
 from reflex.constants import Dirs, Hooks, Imports
 from reflex.constants.compiler import CompileVars
 from reflex.constants.compiler import CompileVars
@@ -90,7 +91,7 @@ def default_connection_error() -> list[str | Var | Component]:
     ]
     ]
 
 
 
 
-class ConnectionToaster(Toaster):
+class ConnectionToaster(Fragment):
     """A connection toaster component."""
     """A connection toaster component."""
 
 
     def add_hooks(self) -> list[str | Var]:
     def add_hooks(self) -> list[str | Var]:
@@ -113,11 +114,11 @@ class ConnectionToaster(Toaster):
         if environment.REFLEX_DOES_BACKEND_COLD_START.get():
         if environment.REFLEX_DOES_BACKEND_COLD_START.get():
             loading_message = Var.create("Backend is starting.")
             loading_message = Var.create("Backend is starting.")
             backend_is_loading_toast_var = Var(
             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_not_responding = Var.create("Backend is not responding.")
             backend_is_down_toast_var = Var(
             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(
             toast_var = Var(
                 f"""
                 f"""
@@ -138,10 +139,11 @@ setTimeout(() => {{
                 f"Cannot connect to server: {connection_error}."
                 f"Cannot connect to server: {connection_error}."
             )
             )
             toast_var = Var(
             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 = [
         individual_hooks = [
+            Var(f"const toast = {toast_ref};"),
             f"const toast_props = {LiteralVar.create(props)!s};",
             f"const toast_props = {LiteralVar.create(props)!s};",
             "const [userDismissed, setUserDismissed] = useState(false);",
             "const [userDismissed, setUserDismissed] = useState(false);",
             "const [waitedForBackend, setWaitedForBackend] = useState(false);",
             "const [waitedForBackend, setWaitedForBackend] = useState(false);",
@@ -163,7 +165,7 @@ setTimeout(() => {{
             {toast_var!s}
             {toast_var!s}
         }}
         }}
     }} else {{
     }} else {{
-        toast.dismiss("{toast_id}");
+        toast?.dismiss("{toast_id}");
         setUserDismissed(false);  // after reconnection reset dismissed state
         setUserDismissed(false);  // after reconnection reset dismissed state
     }}
     }}
 }}
 }}
@@ -189,7 +191,6 @@ setTimeout(() => {{
         Returns:
         Returns:
             The connection toaster component.
             The connection toaster component.
         """
         """
-        Toaster.is_used = True
         return super().create(*children, **props)
         return super().create(*children, **props)
 
 
 
 

+ 2 - 51
reflex/components/core/banner.pyi

@@ -5,10 +5,10 @@
 # ------------------------------------------------------
 # ------------------------------------------------------
 from typing import Any, Dict, Literal, Optional, Union, overload
 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.component import Component
 from reflex.components.el.elements.typography import Div
 from reflex.components.el.elements.typography import Div
 from reflex.components.lucide.icon import Icon
 from reflex.components.lucide.icon import Icon
-from reflex.components.sonner.toast import Toaster, ToastProps
 from reflex.constants.compiler import CompileVars
 from reflex.constants.compiler import CompileVars
 from reflex.event import EventType
 from reflex.event import EventType
 from reflex.style import Style
 from reflex.style import Style
@@ -41,48 +41,13 @@ class WebsocketTargetURL(Var):
 
 
 def default_connection_error() -> list[str | Var | Component]: ...
 def default_connection_error() -> list[str | Var | Component]: ...
 
 
-class ConnectionToaster(Toaster):
+class ConnectionToaster(Fragment):
     def add_hooks(self) -> list[str | Var]: ...
     def add_hooks(self) -> list[str | Var]: ...
     @overload
     @overload
     @classmethod
     @classmethod
     def create(  # type: ignore
     def create(  # type: ignore
         cls,
         cls,
         *children,
         *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,
         style: Optional[Style] = None,
         key: Optional[Any] = None,
         key: Optional[Any] = None,
         id: Optional[Any] = None,
         id: Optional[Any] = None,
@@ -110,20 +75,6 @@ class ConnectionToaster(Toaster):
 
 
         Args:
         Args:
             *children: The children of the component.
             *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.
             style: The style of the component.
             key: A unique key for the component.
             key: A unique key for the component.
             id: The id for the component.
             id: The id for the component.

+ 22 - 11
reflex/components/sonner/toast.py

@@ -2,7 +2,7 @@
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
-from typing import Any, ClassVar, Literal, Optional, Union
+from typing import Any, Literal, Optional, Union
 
 
 from reflex.base import Base
 from reflex.base import Base
 from reflex.components.component import Component, ComponentNamespace
 from reflex.components.component import Component, ComponentNamespace
@@ -17,6 +17,7 @@ from reflex.utils.serializers import serializer
 from reflex.vars import VarData
 from reflex.vars import VarData
 from reflex.vars.base import LiteralVar, Var
 from reflex.vars.base import LiteralVar, Var
 from reflex.vars.function import FunctionVar
 from reflex.vars.function import FunctionVar
+from reflex.vars.number import ternary_operation
 from reflex.vars.object import ObjectVar
 from reflex.vars.object import ObjectVar
 
 
 LiteralPosition = Literal[
 LiteralPosition = Literal[
@@ -217,9 +218,6 @@ 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.
     # 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]
     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]:
     def add_hooks(self) -> list[Var | str]:
         """Add hooks for the toaster component.
         """Add hooks for the toaster component.
 
 
@@ -241,13 +239,17 @@ class Toaster(Component):
 
 
     @staticmethod
     @staticmethod
     def send_toast(
     def send_toast(
-        message: str | Var = "", level: str | None = None, **props
+        message: str | Var = "",
+        level: str | None = None,
+        fallback_to_alert: bool = False,
+        **props,
     ) -> EventSpec:
     ) -> EventSpec:
         """Send a toast message.
         """Send a toast message.
 
 
         Args:
         Args:
             message: The message to display.
             message: The message to display.
             level: The level of the toast.
             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.
             **props: The options for the toast.
 
 
         Raises:
         Raises:
@@ -256,11 +258,6 @@ class Toaster(Component):
         Returns:
         Returns:
             The toast event.
             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 = (
         toast_command = (
             ObjectVar.__getattr__(toast_ref.to(dict), level) if level else toast_ref
             ObjectVar.__getattr__(toast_ref.to(dict), level) if level else toast_ref
         ).to(FunctionVar)
         ).to(FunctionVar)
@@ -277,6 +274,21 @@ class Toaster(Component):
         else:
         else:
             toast = toast_command.call(message)
             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)
         return run_script(toast)
 
 
     @staticmethod
     @staticmethod
@@ -379,7 +391,6 @@ class Toaster(Component):
         Returns:
         Returns:
             The toaster component.
             The toaster component.
         """
         """
-        cls.is_used = True
         return super().create(*children, **props)
         return super().create(*children, **props)
 
 
 
 

+ 10 - 5
reflex/components/sonner/toast.pyi

@@ -3,7 +3,7 @@
 # ------------------- DO NOT EDIT ----------------------
 # ------------------- DO NOT EDIT ----------------------
 # This file was generated by `reflex/utils/pyi_generator.py`!
 # This file was generated by `reflex/utils/pyi_generator.py`!
 # ------------------------------------------------------
 # ------------------------------------------------------
-from typing import Any, ClassVar, Dict, Literal, Optional, Union, overload
+from typing import Any, Dict, Literal, Optional, Union, overload
 
 
 from reflex.base import Base
 from reflex.base import Base
 from reflex.components.component import Component, ComponentNamespace
 from reflex.components.component import Component, ComponentNamespace
@@ -60,12 +60,13 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
     def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
     def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
 
 
 class Toaster(Component):
 class Toaster(Component):
-    is_used: ClassVar[bool] = False
-
     def add_hooks(self) -> list[Var | str]: ...
     def add_hooks(self) -> list[Var | str]: ...
     @staticmethod
     @staticmethod
     def send_toast(
     def send_toast(
-        message: str | Var = "", level: str | None = None, **props
+        message: str | Var = "",
+        level: str | None = None,
+        fallback_to_alert: bool = False,
+        **props,
     ) -> EventSpec: ...
     ) -> EventSpec: ...
     @staticmethod
     @staticmethod
     def toast_info(message: str | Var = "", **kwargs: Any): ...
     def toast_info(message: str | Var = "", **kwargs: Any): ...
@@ -185,13 +186,17 @@ class ToastNamespace(ComponentNamespace):
 
 
     @staticmethod
     @staticmethod
     def __call__(
     def __call__(
-        message: Union[str, Var] = "", level: Optional[str] = None, **props
+        message: Union[str, Var] = "",
+        level: Optional[str] = None,
+        fallback_to_alert: bool = False,
+        **props,
     ) -> "EventSpec":
     ) -> "EventSpec":
         """Send a toast message.
         """Send a toast message.
 
 
         Args:
         Args:
             message: The message to display.
             message: The message to display.
             level: The level of the toast.
             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.
             **props: The options for the toast.
 
 
         Raises:
         Raises:

+ 14 - 24
tests/units/test_state.py

@@ -35,7 +35,6 @@ import reflex.config
 from reflex import constants
 from reflex import constants
 from reflex.app import App
 from reflex.app import App
 from reflex.base import Base
 from reflex.base import Base
-from reflex.components.sonner.toast import Toaster
 from reflex.constants import CompileVars, RouteVar, SocketEvent
 from reflex.constants import CompileVars, RouteVar, SocketEvent
 from reflex.event import Event, EventHandler
 from reflex.event import Event, EventHandler
 from reflex.state import (
 from reflex.state import (
@@ -1613,29 +1612,20 @@ async def test_state_with_invalid_yield(capsys, mock_app):
         rx.event.Event(token="fake_token", name="invalid_handler")
         rx.event.Event(token="fake_token", name="invalid_handler")
     ):
     ):
         assert not update.delta
         assert not update.delta
-        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="",
-            )
+        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="",
+        )
     captured = capsys.readouterr()
     captured = capsys.readouterr()
     assert "must only return/yield: None, Events or other EventHandlers" in captured.out
     assert "must only return/yield: None, Events or other EventHandlers" in captured.out