simplify toast banner logic

This commit is contained in:
Khaleel Al-Adhami 2025-02-20 12:47:25 -08:00
parent 836e8f8ce9
commit 87f630e906
6 changed files with 68 additions and 125 deletions

View File

@ -84,7 +84,6 @@ 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 +143,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
EventSpec: The window alert event.
"""
from reflex.components.sonner.toast import Toaster, toast
from reflex.components.sonner.toast import toast
error = traceback.format_exc()
@ -155,18 +154,16 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
if is_prod_mode()
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]:
@ -1100,10 +1097,6 @@ 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}")
@ -1133,20 +1126,6 @@ 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:
@ -1207,6 +1186,20 @@ class App(MiddlewareMixin, LifespanMixin):
# Add the custom components from the page to the set.
custom_components |= component._get_all_custom_components()
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
# Add the app wraps to the app.
for key, app_wrap in self.app_wraps.items():
component = app_wrap(self._state is not None)

View File

@ -5,6 +5,7 @@ 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
@ -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.typography.text import Text
from reflex.components.sonner.toast import Toaster, ToastProps
from reflex.components.sonner.toast import ToastProps
from reflex.config import environment
from reflex.constants import Dirs, Hooks, Imports
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."""
def add_hooks(self) -> list[str | Var]:
@ -189,7 +190,6 @@ setTimeout(() => {{
Returns:
The connection toaster component.
"""
Toaster.is_used = True
return super().create(*children, **props)

View File

@ -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,48 +41,13 @@ class WebsocketTargetURL(Var):
def default_connection_error() -> list[str | Var | Component]: ...
class ConnectionToaster(Toaster):
class ConnectionToaster(Fragment):
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,
@ -110,20 +75,6 @@ class ConnectionToaster(Toaster):
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.

View File

@ -2,7 +2,7 @@
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.components.component import Component, ComponentNamespace
@ -17,6 +17,7 @@ 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[
@ -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.
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.
@ -241,13 +239,17 @@ class Toaster(Component):
@staticmethod
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:
"""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:
@ -256,11 +258,6 @@ 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)
@ -277,6 +274,13 @@ class Toaster(Component):
else:
toast = toast_command.call(message)
if fallback_to_alert:
toast = ternary_operation(
toast_ref.bool(),
toast,
FunctionVar("window.alert").call(message),
)
return run_script(toast)
@staticmethod

View File

@ -3,7 +3,7 @@
# ------------------- DO NOT EDIT ----------------------
# 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.components.component import Component, ComponentNamespace
@ -60,12 +60,13 @@ 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, **props
message: str | Var = "",
level: str | None = None,
fallback_to_alert: bool = False,
**props,
) -> EventSpec: ...
@staticmethod
def toast_info(message: str | Var = "", **kwargs: Any): ...
@ -185,13 +186,17 @@ class ToastNamespace(ComponentNamespace):
@staticmethod
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":
"""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:

View File

@ -35,7 +35,6 @@ 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 (
@ -1613,29 +1612,20 @@ async def test_state_with_invalid_yield(capsys, mock_app):
rx.event.Event(token="fake_token", name="invalid_handler")
):
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()
assert "must only return/yield: None, Events or other EventHandlers" in captured.out