Skip to content

Commit 725e47c

Browse files
adhami3310masenf
authored andcommitted
simplify toast banner logic (#4853)
* simplify toast banner logic * expose toast * default back to title and desc, and replace brs with new lines
1 parent 7855a59 commit 725e47c

File tree

6 files changed

+80
-131
lines changed

6 files changed

+80
-131
lines changed

reflex/app.py

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
from reflex.components.core.sticky import sticky
7676
from reflex.components.core.upload import Upload, get_upload_dir
7777
from reflex.components.radix import themes
78+
from reflex.components.sonner.toast import toast
7879
from reflex.config import ExecutorType, environment, get_config
7980
from reflex.event import (
8081
_EVENT_FIELDS,
@@ -84,7 +85,6 @@
8485
EventType,
8586
IndividualEventType,
8687
get_hydrate_event,
87-
window_alert,
8888
)
8989
from reflex.model import Model, get_db_status
9090
from reflex.page import DECORATED_PAGES
@@ -144,7 +144,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
144144
EventSpec: The window alert event.
145145
146146
"""
147-
from reflex.components.sonner.toast import Toaster, toast
147+
from reflex.components.sonner.toast import toast
148148

149149
error = traceback.format_exc()
150150

@@ -155,18 +155,16 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
155155
if is_prod_mode()
156156
else [f"{type(exception).__name__}: {exception}.", "See logs for details."]
157157
)
158-
if Toaster.is_used:
159-
return toast(
160-
"An error occurred.",
161-
level="error",
162-
description="<br/>".join(error_message),
163-
position="top-center",
164-
id="backend_error",
165-
style={"width": "500px"},
166-
)
167-
else:
168-
error_message.insert(0, "An error occurred.")
169-
return window_alert("\n".join(error_message))
158+
159+
return toast(
160+
"An error occurred.",
161+
level="error",
162+
fallback_to_alert=True,
163+
description="<br/>".join(error_message),
164+
position="top-center",
165+
id="backend_error",
166+
style={"width": "500px"},
167+
)
170168

171169

172170
def extra_overlay_function() -> Optional[Component]:
@@ -414,7 +412,7 @@ class App(MiddlewareMixin, LifespanMixin):
414412
] = default_backend_exception_handler
415413

416414
# Put the toast provider in the app wrap.
417-
bundle_toaster: bool = True
415+
toaster: Component | None = dataclasses.field(default_factory=toast.provider)
418416

419417
@property
420418
def api(self) -> FastAPI | None:
@@ -1100,10 +1098,6 @@ def get_compilation_time() -> str:
11001098
should_compile = self._should_compile()
11011099

11021100
if not should_compile:
1103-
if self.bundle_toaster:
1104-
from reflex.components.sonner.toast import Toaster
1105-
1106-
Toaster.is_used = True
11071101
with console.timing("Evaluate Pages (Backend)"):
11081102
for route in self._unevaluated_pages:
11091103
console.debug(f"Evaluating page: {route}")
@@ -1133,20 +1127,6 @@ def get_compilation_time() -> str:
11331127
+ adhoc_steps_without_executor,
11341128
)
11351129

1136-
if self.bundle_toaster:
1137-
from reflex.components.component import memo
1138-
from reflex.components.sonner.toast import toast
1139-
1140-
internal_toast_provider = toast.provider()
1141-
1142-
@memo
1143-
def memoized_toast_provider():
1144-
return internal_toast_provider
1145-
1146-
toast_provider = Fragment.create(memoized_toast_provider())
1147-
1148-
app_wrappers[(1, "ToasterProvider")] = toast_provider
1149-
11501130
with console.timing("Evaluate Pages (Frontend)"):
11511131
performance_metrics: list[tuple[str, float]] = []
11521132
for route in self._unevaluated_pages:
@@ -1207,6 +1187,17 @@ def memoized_toast_provider():
12071187
# Add the custom components from the page to the set.
12081188
custom_components |= component._get_all_custom_components()
12091189

1190+
if (toaster := self.toaster) is not None:
1191+
from reflex.components.component import memo
1192+
1193+
@memo
1194+
def memoized_toast_provider():
1195+
return toaster
1196+
1197+
toast_provider = Fragment.create(memoized_toast_provider())
1198+
1199+
app_wrappers[(1, "ToasterProvider")] = toast_provider
1200+
12101201
# Add the app wraps to the app.
12111202
for key, app_wrap in self.app_wraps.items():
12121203
component = app_wrap(self._state is not None)

reflex/components/core/banner.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Optional
66

77
from reflex import constants
8+
from reflex.components.base.fragment import Fragment
89
from reflex.components.component import Component
910
from reflex.components.core.cond import cond
1011
from reflex.components.el.elements.typography import Div
@@ -16,7 +17,7 @@
1617
)
1718
from reflex.components.radix.themes.layout.flex import Flex
1819
from reflex.components.radix.themes.typography.text import Text
19-
from reflex.components.sonner.toast import Toaster, ToastProps
20+
from reflex.components.sonner.toast import ToastProps, toast_ref
2021
from reflex.config import environment
2122
from reflex.constants import Dirs, Hooks, Imports
2223
from reflex.constants.compiler import CompileVars
@@ -90,7 +91,7 @@ def default_connection_error() -> list[str | Var | Component]:
9091
]
9192

9293

93-
class ConnectionToaster(Toaster):
94+
class ConnectionToaster(Fragment):
9495
"""A connection toaster component."""
9596

9697
def add_hooks(self) -> list[str | Var]:
@@ -113,11 +114,11 @@ def add_hooks(self) -> list[str | Var]:
113114
if environment.REFLEX_DOES_BACKEND_COLD_START.get():
114115
loading_message = Var.create("Backend is starting.")
115116
backend_is_loading_toast_var = Var(
116-
f"toast.loading({loading_message!s}, {{...toast_props, description: '', closeButton: false, onDismiss: () => setUserDismissed(true)}},)"
117+
f"toast?.loading({loading_message!s}, {{...toast_props, description: '', closeButton: false, onDismiss: () => setUserDismissed(true)}},)"
117118
)
118119
backend_is_not_responding = Var.create("Backend is not responding.")
119120
backend_is_down_toast_var = Var(
120-
f"toast.error({backend_is_not_responding!s}, {{...toast_props, description: '', onDismiss: () => setUserDismissed(true)}},)"
121+
f"toast?.error({backend_is_not_responding!s}, {{...toast_props, description: '', onDismiss: () => setUserDismissed(true)}},)"
121122
)
122123
toast_var = Var(
123124
f"""
@@ -138,10 +139,11 @@ def add_hooks(self) -> list[str | Var]:
138139
f"Cannot connect to server: {connection_error}."
139140
)
140141
toast_var = Var(
141-
f"toast.error({loading_message!s}, {{...toast_props, onDismiss: () => setUserDismissed(true)}},)"
142+
f"toast?.error({loading_message!s}, {{...toast_props, onDismiss: () => setUserDismissed(true)}},)"
142143
)
143144

144145
individual_hooks = [
146+
Var(f"const toast = {toast_ref};"),
145147
f"const toast_props = {LiteralVar.create(props)!s};",
146148
"const [userDismissed, setUserDismissed] = useState(false);",
147149
"const [waitedForBackend, setWaitedForBackend] = useState(false);",
@@ -163,7 +165,7 @@ def add_hooks(self) -> list[str | Var]:
163165
{toast_var!s}
164166
}}
165167
}} else {{
166-
toast.dismiss("{toast_id}");
168+
toast?.dismiss("{toast_id}");
167169
setUserDismissed(false); // after reconnection reset dismissed state
168170
}}
169171
}}
@@ -189,7 +191,6 @@ def create(cls, *children, **props) -> Component:
189191
Returns:
190192
The connection toaster component.
191193
"""
192-
Toaster.is_used = True
193194
return super().create(*children, **props)
194195

195196

reflex/components/core/banner.pyi

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
# ------------------------------------------------------
66
from typing import Any, Dict, Literal, Optional, Union, overload
77

8+
from reflex.components.base.fragment import Fragment
89
from reflex.components.component import Component
910
from reflex.components.el.elements.typography import Div
1011
from reflex.components.lucide.icon import Icon
11-
from reflex.components.sonner.toast import Toaster, ToastProps
1212
from reflex.constants.compiler import CompileVars
1313
from reflex.event import EventType
1414
from reflex.style import Style
@@ -41,48 +41,13 @@ class WebsocketTargetURL(Var):
4141

4242
def default_connection_error() -> list[str | Var | Component]: ...
4343

44-
class ConnectionToaster(Toaster):
44+
class ConnectionToaster(Fragment):
4545
def add_hooks(self) -> list[str | Var]: ...
4646
@overload
4747
@classmethod
4848
def create( # type: ignore
4949
cls,
5050
*children,
51-
theme: Optional[Union[Var[str], str]] = None,
52-
rich_colors: Optional[Union[Var[bool], bool]] = None,
53-
expand: Optional[Union[Var[bool], bool]] = None,
54-
visible_toasts: Optional[Union[Var[int], int]] = None,
55-
position: Optional[
56-
Union[
57-
Literal[
58-
"bottom-center",
59-
"bottom-left",
60-
"bottom-right",
61-
"top-center",
62-
"top-left",
63-
"top-right",
64-
],
65-
Var[
66-
Literal[
67-
"bottom-center",
68-
"bottom-left",
69-
"bottom-right",
70-
"top-center",
71-
"top-left",
72-
"top-right",
73-
]
74-
],
75-
]
76-
] = None,
77-
close_button: Optional[Union[Var[bool], bool]] = None,
78-
offset: Optional[Union[Var[str], str]] = None,
79-
dir: Optional[Union[Var[str], str]] = None,
80-
hotkey: Optional[Union[Var[str], str]] = None,
81-
invert: Optional[Union[Var[bool], bool]] = None,
82-
toast_options: Optional[Union[ToastProps, Var[ToastProps]]] = None,
83-
gap: Optional[Union[Var[int], int]] = None,
84-
loading_icon: Optional[Union[Icon, Var[Icon]]] = None,
85-
pause_when_page_is_hidden: Optional[Union[Var[bool], bool]] = None,
8651
style: Optional[Style] = None,
8752
key: Optional[Any] = None,
8853
id: Optional[Any] = None,
@@ -110,20 +75,6 @@ class ConnectionToaster(Toaster):
11075
11176
Args:
11277
*children: The children of the component.
113-
theme: the theme of the toast
114-
rich_colors: whether to show rich colors
115-
expand: whether to expand the toast
116-
visible_toasts: the number of toasts that are currently visible
117-
position: the position of the toast
118-
close_button: whether to show the close button
119-
offset: offset of the toast
120-
dir: directionality of the toast (default: ltr)
121-
hotkey: Keyboard shortcut that will move focus to the toaster area.
122-
invert: Dark toasts in light mode and vice versa.
123-
toast_options: These will act as default options for all toasts. See toast() for all available options.
124-
gap: Gap between toasts when expanded
125-
loading_icon: Changes the default loading icon
126-
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.
12778
style: The style of the component.
12879
key: A unique key for the component.
12980
id: The id for the component.

reflex/components/sonner/toast.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import Any, ClassVar, Literal, Optional, Union
5+
from typing import Any, Literal, Optional, Union
66

77
from reflex.base import Base
88
from reflex.components.component import Component, ComponentNamespace
@@ -17,6 +17,7 @@
1717
from reflex.vars import VarData
1818
from reflex.vars.base import LiteralVar, Var
1919
from reflex.vars.function import FunctionVar
20+
from reflex.vars.number import ternary_operation
2021
from reflex.vars.object import ObjectVar
2122

2223
LiteralPosition = Literal[
@@ -217,9 +218,6 @@ class Toaster(Component):
217218
# Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
218219
pause_when_page_is_hidden: Var[bool]
219220

220-
# Marked True when any Toast component is created.
221-
is_used: ClassVar[bool] = False
222-
223221
def add_hooks(self) -> list[Var | str]:
224222
"""Add hooks for the toaster component.
225223
@@ -241,13 +239,17 @@ def add_hooks(self) -> list[Var | str]:
241239

242240
@staticmethod
243241
def send_toast(
244-
message: str | Var = "", level: str | None = None, **props
242+
message: str | Var = "",
243+
level: str | None = None,
244+
fallback_to_alert: bool = False,
245+
**props,
245246
) -> EventSpec:
246247
"""Send a toast message.
247248
248249
Args:
249250
message: The message to display.
250251
level: The level of the toast.
252+
fallback_to_alert: Whether to fallback to an alert if the toaster is not created.
251253
**props: The options for the toast.
252254
253255
Raises:
@@ -256,11 +258,6 @@ def send_toast(
256258
Returns:
257259
The toast event.
258260
"""
259-
if not Toaster.is_used:
260-
raise ValueError(
261-
"Toaster component must be created before sending a toast. (use `rx.toast.provider()`)"
262-
)
263-
264261
toast_command = (
265262
ObjectVar.__getattr__(toast_ref.to(dict), level) if level else toast_ref
266263
).to(FunctionVar)
@@ -277,6 +274,21 @@ def send_toast(
277274
else:
278275
toast = toast_command.call(message)
279276

277+
if fallback_to_alert:
278+
toast = ternary_operation(
279+
toast_ref.bool(),
280+
toast,
281+
FunctionVar("window.alert").call(
282+
Var.create(
283+
message
284+
if isinstance(message, str) and message
285+
else props.get("title", props.get("description", ""))
286+
)
287+
.to(str)
288+
.replace("<br/>", "\n")
289+
),
290+
)
291+
280292
return run_script(toast)
281293

282294
@staticmethod
@@ -379,7 +391,6 @@ def create(cls, *children: Any, **props: Any) -> Component:
379391
Returns:
380392
The toaster component.
381393
"""
382-
cls.is_used = True
383394
return super().create(*children, **props)
384395

385396

reflex/components/sonner/toast.pyi

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# ------------------- DO NOT EDIT ----------------------
44
# This file was generated by `reflex/utils/pyi_generator.py`!
55
# ------------------------------------------------------
6-
from typing import Any, ClassVar, Dict, Literal, Optional, Union, overload
6+
from typing import Any, Dict, Literal, Optional, Union, overload
77

88
from reflex.base import Base
99
from reflex.components.component import Component, ComponentNamespace
@@ -60,12 +60,13 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
6060
def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...
6161

6262
class Toaster(Component):
63-
is_used: ClassVar[bool] = False
64-
6563
def add_hooks(self) -> list[Var | str]: ...
6664
@staticmethod
6765
def send_toast(
68-
message: str | Var = "", level: str | None = None, **props
66+
message: str | Var = "",
67+
level: str | None = None,
68+
fallback_to_alert: bool = False,
69+
**props,
6970
) -> EventSpec: ...
7071
@staticmethod
7172
def toast_info(message: str | Var = "", **kwargs: Any): ...
@@ -185,13 +186,17 @@ class ToastNamespace(ComponentNamespace):
185186

186187
@staticmethod
187188
def __call__(
188-
message: Union[str, Var] = "", level: Optional[str] = None, **props
189+
message: Union[str, Var] = "",
190+
level: Optional[str] = None,
191+
fallback_to_alert: bool = False,
192+
**props,
189193
) -> "EventSpec":
190194
"""Send a toast message.
191195
192196
Args:
193197
message: The message to display.
194198
level: The level of the toast.
199+
fallback_to_alert: Whether to fallback to an alert if the toaster is not created.
195200
**props: The options for the toast.
196201
197202
Raises:

0 commit comments

Comments
 (0)