Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add toast component #3186

Merged
merged 14 commits into from
May 3, 2024
1 change: 1 addition & 0 deletions reflex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"ordered_list",
"moment",
"logo",
"toast",
]

_MAPPING = {
Expand Down
1 change: 1 addition & 0 deletions reflex/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ from reflex.components import unordered_list as unordered_list
from reflex.components import ordered_list as ordered_list
from reflex.components import moment as moment
from reflex.components import logo as logo
from reflex.components import toast as toast
from reflex.components.component import Component as Component
from reflex.components.component import NoSSRComponent as NoSSRComponent
from reflex.components.component import memo as memo
Expand Down
1 change: 1 addition & 0 deletions reflex/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .plotly import *
from .radix import *
from .react_player import *
from .sonner import *
from .suneditor import *

icon = lucide.icon
4 changes: 1 addition & 3 deletions reflex/components/chakra/datadisplay/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ class List(ChakraComponent):
style_type: Var[str]

@classmethod
def create(
cls, *children, items: list | Var[list] | None = None, **props
) -> Component:
def create(cls, *children, items: Var[list] | None = None, **props) -> Component:
"""Create a list component.

Args:
Expand Down
6 changes: 3 additions & 3 deletions reflex/components/chakra/datadisplay/list.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class List(ChakraComponent):
def create( # type: ignore
cls,
*children,
items: Optional[list | Var[list] | None] = None,
items: Optional[Union[Var[list], list]] = None,
spacing: Optional[Union[Var[str], str]] = None,
style_position: Optional[Union[Var[str], str]] = None,
style_type: Optional[Union[Var[str], str]] = None,
Expand Down Expand Up @@ -178,7 +178,7 @@ class OrderedList(List):
def create( # type: ignore
cls,
*children,
items: Optional[list | Var[list] | None] = None,
items: Optional[Union[Var[list], list]] = None,
spacing: Optional[Union[Var[str], str]] = None,
style_position: Optional[Union[Var[str], str]] = None,
style_type: Optional[Union[Var[str], str]] = None,
Expand Down Expand Up @@ -262,7 +262,7 @@ class UnorderedList(List):
def create( # type: ignore
cls,
*children,
items: Optional[list | Var[list] | None] = None,
items: Optional[Union[Var[list], list]] = None,
spacing: Optional[Union[Var[str], str]] = None,
style_position: Optional[Union[Var[str], str]] = None,
style_type: Optional[Union[Var[str], str]] = None,
Expand Down
2 changes: 1 addition & 1 deletion reflex/components/chakra/forms/pininput.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class PinInputField(ChakraComponent):
def create( # type: ignore
cls,
*children,
index: Optional[Var[int]] = None,
index: Optional[Union[Var[int], int]] = None,
name: Optional[Union[Var[str], str]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
Expand Down
4 changes: 2 additions & 2 deletions reflex/components/radix/primitives/accordion.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,10 +314,10 @@ class AccordionRoot(AccordionComponent):
type: Var[LiteralAccordionType]

# The value of the item to expand.
value: Var[Optional[Union[str, List[str]]]]
value: Var[Union[str, List[str]]]

# The default value of the item to expand.
default_value: Var[Optional[Union[str, List[str]]]]
default_value: Var[Union[str, List[str]]]

# Whether or not the accordion is collapsible.
collapsible: Var[bool]
Expand Down
4 changes: 2 additions & 2 deletions reflex/components/radix/primitives/accordion.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ class AccordionItem(AccordionComponent):
def create( # type: ignore
cls,
*children,
header: Optional[Component | Var] = None,
content: Optional[Component | Var] = None,
header: Optional[Union[Component, Var]] = None,
content: Optional[Union[Component, Var]] = None,
value: Optional[Union[Var[str], str]] = None,
disabled: Optional[Union[Var[bool], bool]] = None,
as_child: Optional[Union[Var[bool], bool]] = None,
Expand Down
2 changes: 1 addition & 1 deletion reflex/components/radix/themes/base.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ class Theme(RadixThemesComponent):
def create( # type: ignore
cls,
*children,
color_mode: Optional[LiteralAppearance | None] = None,
color_mode: Optional[Literal["inherit", "light", "dark"]] = None,
theme_panel: Optional[bool] = False,
has_background: Optional[Union[Var[bool], bool]] = None,
appearance: Optional[
Expand Down
4 changes: 3 additions & 1 deletion reflex/components/radix/themes/color_mode.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ class ColorModeIconButton(IconButton):
def create( # type: ignore
cls,
*children,
position: Optional[LiteralPosition | None] = None,
position: Optional[
Literal["top-left", "top-right", "bottom-left", "bottom-right"]
] = None,
as_child: Optional[Union[Var[bool], bool]] = None,
size: Optional[
Union[Var[Literal["1", "2", "3", "4"]], Literal["1", "2", "3", "4"]]
Expand Down
4 changes: 2 additions & 2 deletions reflex/components/radix/themes/layout/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class BaseList(Component):
def create(
cls,
*children,
items: Optional[Union[Var[Iterable], Iterable]] = None,
items: Optional[Var[Iterable]] = None,
**props,
):
"""Create a list component.
Expand All @@ -68,7 +68,7 @@ def create(
if isinstance(items, Var):
children = [Foreach.create(items, ListItem.create)]
else:
children = [ListItem.create(item) for item in items]
children = [ListItem.create(item) for item in items] # type: ignore
props["list_style_position"] = "outside"
props["direction"] = "column"
style = props.setdefault("style", {})
Expand Down
4 changes: 2 additions & 2 deletions reflex/components/radix/themes/layout/list.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class BaseList(Component):
def create( # type: ignore
cls,
*children,
items: Optional[Union[Union[Var[Iterable], Iterable], Iterable]] = None,
items: Optional[Union[Var[Iterable], Iterable]] = None,
list_style_type: Optional[
Union[
Var[
Expand Down Expand Up @@ -600,7 +600,7 @@ class List(ComponentNamespace):
@staticmethod
def __call__(
*children,
items: Optional[Union[Union[Var[Iterable], Iterable], Iterable]] = None,
items: Optional[Union[Var[Iterable], Iterable]] = None,
list_style_type: Optional[
Union[
Var[
Expand Down
3 changes: 3 additions & 0 deletions reflex/components/sonner/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Init file for the sonner component."""

from .toast import toast
224 changes: 224 additions & 0 deletions reflex/components/sonner/toast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
"""Sonner toast component."""

from __future__ import annotations

from typing import Dict, Literal, Optional

from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
from reflex.components.lucide.icon import Icon
from reflex.event import EventSpec, call_script
from reflex.style import color_mode
from reflex.utils import format
from reflex.utils.imports import ImportVar
from reflex.utils.serializers import serialize
from reflex.vars import Var, VarData

LiteralPosition = Literal[
"top-left",
"top-center",
"top-right",
"bottom-left",
"bottom-center",
"bottom-right",
]


toast_ref = Var.create_safe("refs['__toast']")


class PropsBase(Base):
"""Base class for all props classes."""

def json(self) -> str:
"""Convert the object to a json string.

Returns:
The object as a json string.
"""
from reflex.utils.serializers import serialize

return self.__config__.json_dumps(
{format.to_camel_case(key): value for key, value in self.dict().items()},
default=serialize,
)


class ToastProps(PropsBase):
"""Props for the toast component."""

description: str = ""
close_button: bool = False
invert: bool = False
Lendemor marked this conversation as resolved.
Show resolved Hide resolved
important: bool = False
Lendemor marked this conversation as resolved.
Show resolved Hide resolved
duration: int = 4000
position: LiteralPosition = "bottom-right"
dismissible: bool = True
icon: Optional[Icon] = None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if i pass a lucide icon here, it gives a frontend error and crashes

Unhandled Runtime Error
Error: Objects are not valid as a React child (found: object with keys {children, library, lib_dependencies, transpile_packages, tag, style, event_triggers, alias, is_default, key, id, class_name, special_props, autofocus, custom_attrs, State, size, color}). If you meant to render a collection of children, use an array instead.

action: str = ""
cancel: str = ""
Lendemor marked this conversation as resolved.
Show resolved Hide resolved
id: str = ""
unstyled: bool = False
action_button_styles: Dict[str, str] = {}
cancel_button_styles: Dict[str, str] = {}


class Toaster(Component):
"""A Toaster Component for displaying toast notifications."""

library = "sonner@1.4.41"

tag = "Toaster"

# the theme of the toast
theme: Var[str] = color_mode

# whether to show rich colors
rich_colors: Var[bool] = Var.create_safe(True)

# whether to expand the toast
expand: Var[bool] = Var.create_safe(True)

# the number of toasts that are currently visible
visibleToasts: Var[int]
Lendemor marked this conversation as resolved.
Show resolved Hide resolved

# the position of the toast
position: Var[LiteralPosition] = Var.create_safe("bottom-right")

# whether to show the close button
close_button: Var[bool] = Var.create_safe(False)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When i set this to True, the toasts still don't have a close button


# offset of the toast
offset: Var[str]

# directionality of the toast (default: ltr)
dir: Var[str]

hotkey: Var[str]

invert: Var[bool]

toast_options: Var[ToastProps]
Lendemor marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When i set defaults here, they do not seem to be respected.

I tried setting it like

rx.toast.provider(toast_options=rx.toast.options(close_button=True, duration=300), close_button=True)

And the toasts still use the default 4000ms duration and do not have close buttons, unless i pass no options at all.

For example, i'm trying to toast like this

                    yield rx.toast.warning(
                        f"Removed {item.text} from {list_name}",
                        description="Whoops, this was irreversible"
                    )

It seems that any options passed myself overwrite all of the toaster options with the original defaults.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so i updated ToastProps to make all of the fields optional, which seemed to help; but now i'm finding that some of the fields in ToastProps don't take effect when passed to the toast_options prop.

  • description
  • position
  • close_button
    (maybe others, didn't try them all)


gap: Var[int]

loadingIcon: Var[Icon]

pause_when_page_is_hidden: Var[bool]

def _get_hooks(self) -> Var[str]:
hook = Var.create_safe(f"{toast_ref} = toast", _var_is_local=True)
hook._var_data = VarData( # type: ignore
imports={
"/utils/state": [ImportVar(tag="refs")],
self.library: [ImportVar(tag="toast", install=False)],
}
)
return hook

@staticmethod
def send_toast(message: str, level: str | None = None, **props) -> EventSpec:
"""Send a toast message.

Args:
message: The message to display.
level: The level of the toast.
**props: The options for the toast.

Returns:
The toast event.
"""
toast_command = f"{toast_ref}.{level}" if level is not None else toast_ref
if props:
args = serialize(ToastProps(**props))
toast = f"{toast_command}(`{message}`, {args})"
else:
toast = f"{toast_command}(`{message}`)"

toast_action = Var.create(toast, _var_is_string=False, _var_is_local=True)
return call_script(toast_action) # type: ignore

@staticmethod
def toast_info(message: str, **kwargs):
"""Display an info toast message.

Args:
message: The message to display.
kwargs: Additional toast props.

Returns:
The toast event.
"""
return Toaster.send_toast(message, level="info", **kwargs)

@staticmethod
def toast_warning(message: str, **kwargs):
"""Display a warning toast message.

Args:
message: The message to display.
kwargs: Additional toast props.

Returns:
The toast event.
"""
return Toaster.send_toast(message, level="warning", **kwargs)

@staticmethod
def toast_error(message: str, **kwargs):
"""Display an error toast message.

Args:
message: The message to display.
kwargs: Additional toast props.

Returns:
The toast event.
"""
return Toaster.send_toast(message, level="error", **kwargs)

@staticmethod
def toast_success(message: str, **kwargs):
"""Display a success toast message.

Args:
message: The message to display.
kwargs: Additional toast props.

Returns:
The toast event.
"""
return Toaster.send_toast(message, level="success", **kwargs)


# TODO: figure out why loading toast stay open forever
# def toast_loading(message: str, **kwargs):
# return _toast(message, level="loading", **kwargs)


# def _toast(message: str, level: str | None = None, **toast_args):
# toast_command = f"{toast_ref}.{level}" if level is not None else toast_ref
# if toast_args:
# args = serialize(ToastProps(**toast_args))
# toast = f"{toast_command}(`{message}`, {args})"
# else:
# toast = f"{toast_command}(`{message}`)"

# toast_action = Var.create(toast, _var_is_string=False, _var_is_local=True)
# return call_script(toast_action) # type: ignore


class ToastNamespace(ComponentNamespace):
"""Namespace for toast components."""

provider = staticmethod(Toaster.create)
options = staticmethod(ToastProps)
info = staticmethod(Toaster.toast_info)
warning = staticmethod(Toaster.toast_warning)
error = staticmethod(Toaster.toast_error)
success = staticmethod(Toaster.toast_success)
# loading = staticmethod(toast_loading)
__call__ = staticmethod(Toaster.send_toast)


toast = ToastNamespace()
Loading
Loading