simplify toast banner logic (#4853)
* simplify toast banner logic * expose toast * default back to title and desc, and replace brs with new lines
This commit is contained in:
parent
836e8f8ce9
commit
8943341605
@ -75,6 +75,7 @@ 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,
|
||||
@ -84,7 +85,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 +144,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 +155,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]:
|
||||
@ -414,7 +412,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
||||
] = default_backend_exception_handler
|
||||
|
||||
# Put the toast provider in the app wrap.
|
||||
bundle_toaster: bool = True
|
||||
toaster: Component | None = dataclasses.field(default_factory=toast.provider)
|
||||
|
||||
@property
|
||||
def api(self) -> FastAPI | None:
|
||||
@ -1100,10 +1098,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 +1127,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 +1187,17 @@ 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)
|
||||
|
@ -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, toast_ref
|
||||
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]:
|
||||
@ -113,11 +114,11 @@ class ConnectionToaster(Toaster):
|
||||
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"""
|
||||
@ -138,10 +139,11 @@ 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);",
|
||||
@ -163,7 +165,7 @@ setTimeout(() => {{
|
||||
{toast_var!s}
|
||||
}}
|
||||
}} else {{
|
||||
toast.dismiss("{toast_id}");
|
||||
toast?.dismiss("{toast_id}");
|
||||
setUserDismissed(false); // after reconnection reset dismissed state
|
||||
}}
|
||||
}}
|
||||
@ -189,7 +191,6 @@ 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,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.
|
||||
|
@ -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,21 @@ 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
|
||||
@ -379,7 +391,6 @@ 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, 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:
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user